TLS and Graphics, what more?
This commit is contained in:
@@ -11,9 +11,13 @@ public class ConnectionConfig {
|
||||
private String luName = null;
|
||||
private boolean extendedDataStream = true;
|
||||
private boolean useTls = false;
|
||||
private boolean tlsVerifyCert = true;
|
||||
private org.lib3270j.tls.TlsCertificateVerifier certificateVerifier = null;
|
||||
private String sslProtocol = "TLS";
|
||||
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;
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
@@ -22,12 +26,25 @@ public class ConnectionConfig {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, boolean useTls) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.useTls = useTls;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, TerminalModel model) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, TerminalModel model, boolean useTls) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.model = model;
|
||||
this.useTls = useTls;
|
||||
}
|
||||
|
||||
public String getHost() { return host; }
|
||||
public void setHost(String host) { this.host = host; }
|
||||
|
||||
@@ -46,15 +63,79 @@ public class ConnectionConfig {
|
||||
public boolean isUseTls() { return useTls; }
|
||||
public void setUseTls(boolean useTls) { this.useTls = useTls; }
|
||||
|
||||
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
|
||||
public void setTlsVerifyCert(boolean verify) { this.tlsVerifyCert = verify; }
|
||||
|
||||
public org.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
|
||||
public void setCertificateVerifier(org.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; }
|
||||
|
||||
public String getSslProtocol() { return sslProtocol; }
|
||||
public void setSslProtocol(String protocol) { this.sslProtocol = protocol; }
|
||||
|
||||
public int getConnectTimeoutMs() { return connectTimeoutMs; }
|
||||
public void setConnectTimeoutMs(int ms) { this.connectTimeoutMs = ms; }
|
||||
|
||||
public int getNopIntervalSeconds() { return nopIntervalSeconds; }
|
||||
public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; }
|
||||
|
||||
public org.lib3270j.graphics.GraphicsMode getGraphicsMode() { return graphicsMode; }
|
||||
public void setGraphicsMode(org.lib3270j.graphics.GraphicsMode mode) {
|
||||
this.graphicsMode = (mode != null) ? mode : org.lib3270j.graphics.GraphicsMode.NONE;
|
||||
}
|
||||
|
||||
public String getTerminalName() { return terminalName; }
|
||||
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.
|
||||
*/
|
||||
public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) {
|
||||
if (hostStr == null || hostStr.trim().isEmpty()) {
|
||||
return new ConnectionConfig("localhost", defaultPort, defaultModel);
|
||||
}
|
||||
String s = hostStr.trim();
|
||||
boolean tls = false;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
String host = s;
|
||||
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
|
||||
|
||||
// Check for host:port (handle IPv6 [::1]:port)
|
||||
if (s.startsWith("[") && s.contains("]")) {
|
||||
int closeBracket = s.indexOf(']');
|
||||
host = s.substring(1, closeBracket);
|
||||
if (s.length() > closeBracket + 1 && s.charAt(closeBracket + 1) == ':') {
|
||||
try {
|
||||
port = Integer.parseInt(s.substring(closeBracket + 2));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
} else {
|
||||
int colon = s.lastIndexOf(':');
|
||||
if (colon > 0 && colon < s.length() - 1) {
|
||||
try {
|
||||
port = Integer.parseInt(s.substring(colon + 1));
|
||||
host = s.substring(0, colon);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||
config.setUseTls(tls);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective terminal type string to send during negotiation.
|
||||
*/
|
||||
|
||||
@@ -46,12 +46,14 @@ public class Telnet3270Client {
|
||||
this.translator = new EbcdicTranslator();
|
||||
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
|
||||
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
|
||||
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
|
||||
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
||||
|
||||
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
|
||||
dsProcessor.setOutputSender(fsm::send3270Data);
|
||||
dsProcessor.setInputProcessor(inputProcessor);
|
||||
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,6 +124,16 @@ public class Telnet3270Client {
|
||||
/** Get the connection config. */
|
||||
public ConnectionConfig getConfig() { return config; }
|
||||
|
||||
/** Set a custom or interactive TLS certificate verifier callback. */
|
||||
public void setTlsCertificateVerifier(org.lib3270j.tls.TlsCertificateVerifier verifier) {
|
||||
config.setCertificateVerifier(verifier);
|
||||
}
|
||||
|
||||
/** Get the active SSLSession if connected over TLS, or null. */
|
||||
public javax.net.ssl.SSLSession getSslSession() {
|
||||
return connection.getSslSession();
|
||||
}
|
||||
|
||||
// ========== Convenience input methods ==========
|
||||
|
||||
/** Type a character at the cursor position. */
|
||||
@@ -210,4 +222,16 @@ public class Telnet3270Client {
|
||||
public void sysReq() { inputProcessor.sysReq(); }
|
||||
/** Reset (unlock keyboard). */
|
||||
public void reset() { inputProcessor.reset(); }
|
||||
|
||||
public org.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||
return dsProcessor.getProgramSymbolManager();
|
||||
}
|
||||
|
||||
public org.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
|
||||
return dsProcessor.getGraphicsPlane();
|
||||
}
|
||||
|
||||
public org.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||
return dsProcessor.getGocaDecoder();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,20 @@ public class EbcdicTranslator {
|
||||
return (char) CP037_TO_UNICODE[ebc & 0xFF];
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper to translate EBCDIC byte to Unicode character.
|
||||
*/
|
||||
public static char toUnicode(int ebc) {
|
||||
return (char) CP037_TO_UNICODE[ebc & 0xFF];
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper to translate EBCDIC byte to ASCII character.
|
||||
*/
|
||||
public static char ebcdicToAscii(int ebc) {
|
||||
return toUnicode(ebc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate Unicode character to EBCDIC byte.
|
||||
* Returns -1 if the character cannot be mapped.
|
||||
|
||||
@@ -38,6 +38,13 @@ public class DataStreamProcessor {
|
||||
// Input processor reference to manage keyboard locking state
|
||||
private org.lib3270j.input.InputProcessor inputProcessor;
|
||||
|
||||
// Graphics & Programmed Symbols
|
||||
private final org.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new org.lib3270j.graphics.ProgramSymbolManager();
|
||||
private final org.lib3270j.graphics.GraphicsPlane graphicsPlane = new org.lib3270j.graphics.GraphicsPlane(800, 600);
|
||||
private final org.lib3270j.graphics.GocaDecoder gocaDecoder = new org.lib3270j.graphics.GocaDecoder(graphicsPlane);
|
||||
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
||||
private int currentGocaSubtype = 0;
|
||||
|
||||
/** Functional interface for sending output back through the telnet stack. */
|
||||
@FunctionalInterface
|
||||
public interface OutputSender {
|
||||
@@ -50,6 +57,23 @@ public class DataStreamProcessor {
|
||||
this.qrBuilder = new QueryReplyBuilder(screen);
|
||||
this.outputBuffer = new byte[32768];
|
||||
this.outputPos = 0;
|
||||
this.gocaDecoder.setProgramSymbolManager(programSymbolManager);
|
||||
}
|
||||
|
||||
public QueryReplyBuilder getQueryReplyBuilder() {
|
||||
return qrBuilder;
|
||||
}
|
||||
|
||||
public org.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||
return programSymbolManager;
|
||||
}
|
||||
|
||||
public org.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
|
||||
return graphicsPlane;
|
||||
}
|
||||
|
||||
public org.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||
return gocaDecoder;
|
||||
}
|
||||
|
||||
public void setOutputSender(OutputSender sender) {
|
||||
@@ -94,6 +118,7 @@ public class DataStreamProcessor {
|
||||
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();
|
||||
@@ -106,6 +131,7 @@ public class DataStreamProcessor {
|
||||
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();
|
||||
@@ -741,6 +767,7 @@ public class DataStreamProcessor {
|
||||
if (fieldLen >= 4) {
|
||||
boolean alt = (data[pos + 3] & 0xFF) == SF_ER_ALT;
|
||||
screen.erase(alt);
|
||||
graphicsPlane.clear();
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
break;
|
||||
@@ -751,6 +778,7 @@ public class DataStreamProcessor {
|
||||
break;
|
||||
case SF_CREATE_PART:
|
||||
// Acknowledged — we use implicit partition
|
||||
graphicsPlane.clear();
|
||||
break;
|
||||
case SF_OUTBOUND_DS:
|
||||
if (fieldLen > 5) {
|
||||
@@ -765,6 +793,83 @@ public class DataStreamProcessor {
|
||||
log.fine("SF_TRANSFER_DATA received but ftDft is null");
|
||||
}
|
||||
break;
|
||||
case org.lib3270j.graphics.GocaConstants.SF_LOADPS_DIRECT: // 0x06: Load Programmed Symbols direct
|
||||
if (fieldLen > 3) {
|
||||
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
|
||||
if (fieldLen >= 4) {
|
||||
int sfSubId = data[pos + 3] & 0xFF;
|
||||
switch (sfSubId) {
|
||||
case org.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
|
||||
if (fieldLen > 4) {
|
||||
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)
|
||||
case org.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: // 0x10: Object Picture (Picture segments)
|
||||
case org.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Data (GOCA draw orders)
|
||||
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
|
||||
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
|
||||
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
|
||||
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
|
||||
if (flags == 0x80) { // SPAN_FIRST
|
||||
gocaAccumulator.reset();
|
||||
currentGocaSubtype = sfSubId;
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
}
|
||||
} else if (flags == 0x00) { // SPAN_MIDDLE
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
}
|
||||
} else if (flags == 0x40) { // SPAN_LAST
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
}
|
||||
byte[] fullStream = gocaAccumulator.toByteArray();
|
||||
gocaAccumulator.reset();
|
||||
if (currentGocaSubtype == org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
|
||||
} else {
|
||||
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
|
||||
}
|
||||
notifyScreenUpdated();
|
||||
} else { // SPAN_ONLY (0xC0)
|
||||
gocaAccumulator.reset();
|
||||
if (sfSubId == org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
|
||||
} else {
|
||||
gocaDecoder.decodeStream(data, orderOffset, orderLen);
|
||||
}
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
log.fine("Unknown SF 0x0F subtype: " + String.format("0x%02x", sfSubId));
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case org.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
|
||||
case org.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics
|
||||
if (fieldLen > 3) {
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log.fine("Unknown SF id: " + String.format("0x%02x", sfId));
|
||||
break;
|
||||
@@ -887,4 +992,10 @@ public class DataStreamProcessor {
|
||||
l.onScreenSizeChanged(screen.getRows(), screen.getCols());
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyScreenUpdated() {
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onScreenUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ package org.lib3270j.datastream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.lib3270j.graphics.GraphicsMode;
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* Builds Query Reply structured fields in response to host Read Partition queries.
|
||||
* Equivalent to the do_qr_* functions in sf.c.
|
||||
* Matches IBM 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout.
|
||||
*/
|
||||
public class QueryReplyBuilder {
|
||||
|
||||
@@ -21,9 +22,10 @@ public class QueryReplyBuilder {
|
||||
private static final int Yr_3279_2 = 0x0002006f;
|
||||
|
||||
private final ScreenBuffer screen;
|
||||
private GraphicsMode graphicsMode = GraphicsMode.NONE;
|
||||
|
||||
// Supported query reply codes (must match what we send in summary)
|
||||
private static final int[] SUPPORTED_QR = {
|
||||
// Base query reply codes (text mode)
|
||||
private static final int[] SUPPORTED_QR_BASE = {
|
||||
QR_SUMMARY, // 0x80 — summary must list itself
|
||||
QR_USABLE_AREA, // 0x81
|
||||
QR_ALPHA_PART, // 0x84
|
||||
@@ -35,10 +37,45 @@ public class QueryReplyBuilder {
|
||||
QR_IMP_PART, // 0xa6
|
||||
};
|
||||
|
||||
// Vector graphics query reply codes matching HOD DS3270.java line 1723
|
||||
private static final int[] SUPPORTED_QR_VECTOR = {
|
||||
QR_SUMMARY, // 0x80
|
||||
QR_USABLE_AREA, // 0x81
|
||||
QR_ALPHA_PART, // 0x84
|
||||
QR_CHARSETS, // 0x85
|
||||
QR_COLOR, // 0x86
|
||||
QR_HIGHLIGHTING, // 0x87
|
||||
QR_REPLY_MODES, // 0x88
|
||||
QR_SAVE_RESTORE, // 0x8c
|
||||
QR_DDM, // 0x95
|
||||
QR_TRANSPARENCY, // 0x99
|
||||
QR_IMP_PART, // 0xa6
|
||||
QR_RPQ_NAMES, // 0xa8
|
||||
QR_GRAPHICS, // 0xb0
|
||||
QR_GIMAGE, // 0xb1
|
||||
QR_AUX_DEV, // 0xb2
|
||||
QR_OEM_FMT, // 0xb3
|
||||
QR_GCOLOR, // 0xb4
|
||||
QR_GSYMBOLS, // 0xb6
|
||||
};
|
||||
|
||||
public QueryReplyBuilder(ScreenBuffer screen) {
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
public QueryReplyBuilder(ScreenBuffer screen, GraphicsMode graphicsMode) {
|
||||
this.screen = screen;
|
||||
this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE;
|
||||
}
|
||||
|
||||
public GraphicsMode getGraphicsMode() {
|
||||
return graphicsMode;
|
||||
}
|
||||
|
||||
public void setGraphicsMode(GraphicsMode graphicsMode) {
|
||||
this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build all query replies as a single AID_SF + structured field response.
|
||||
*/
|
||||
@@ -75,7 +112,20 @@ public class QueryReplyBuilder {
|
||||
// Implicit Partition
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
|
||||
log.info("Built " + out.size() + " bytes of all query replies");
|
||||
// Vector Graphics QRs if enabled
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
|
||||
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
||||
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames());
|
||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics());
|
||||
appendQueryReply(out, QR_GIMAGE, buildGImage());
|
||||
appendQueryReply(out, QR_AUX_DEV, buildAuxDev());
|
||||
appendOemFmt(out);
|
||||
appendQueryReply(out, QR_GCOLOR, buildGColor());
|
||||
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols());
|
||||
}
|
||||
|
||||
log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")");
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
@@ -116,14 +166,75 @@ public class QueryReplyBuilder {
|
||||
case QR_REPLY_MODES:
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
break;
|
||||
case QR_SAVE_RESTORE:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_DDM:
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
break;
|
||||
case QR_TRANSPARENCY:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_IMP_PART:
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
break;
|
||||
case QR_RPQ_NAMES:
|
||||
case QR_RPQNAMES:
|
||||
appendQueryReply(out, QR_RPQNAMES, new byte[0]);
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_GRAPHICS:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_GIMAGE:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GIMAGE, buildGImage());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_AUX_DEV:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_AUX_DEV, buildAuxDev());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_OEM_FMT:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendOemFmt(out);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_GCOLOR:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GCOLOR, buildGColor());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_GSYMBOLS:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Unsupported query reply code — emit QR_NULL
|
||||
@@ -148,7 +259,8 @@ public class QueryReplyBuilder {
|
||||
|
||||
private byte[] buildSummary() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
for (int code : SUPPORTED_QR) {
|
||||
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
|
||||
for (int code : codes) {
|
||||
out.write(code);
|
||||
}
|
||||
return out.toByteArray();
|
||||
@@ -184,66 +296,81 @@ public class QueryReplyBuilder {
|
||||
private byte[] buildAlphaPartitions(int maxRows) {
|
||||
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
||||
out.write(0x00); // max partitions (1 partition)
|
||||
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
|
||||
out.write((bufSize >> 8) & 0xFF); // total partition storage high
|
||||
out.write(bufSize & 0xFF); // total partition storage low
|
||||
out.write(0x00); // no special features
|
||||
out.write(0x00); // flags
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildCharsets() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(32);
|
||||
if (graphicsMode.isVectorGraphicsEnabled() || graphicsMode == GraphicsMode.NONE) {
|
||||
// Standard 3179G / Base character sets (matches HOD QR_CHARSETS_S_STRING, 27 bytes total)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
|
||||
out.write(0x82); // flags: GE, CGCSGID present
|
||||
out.write(0x00); // more flags
|
||||
out.write(SW_3279_2); // SDW - default char width (9)
|
||||
out.write(14); // SDH - default char height (14)
|
||||
out.write(0x00); // LoadPS format (0x00)
|
||||
out.write(0x00);
|
||||
out.write(0x00);
|
||||
out.write(0x00);
|
||||
out.write(0x07); // DL = 7
|
||||
// Set 0 (Base EBCDIC)
|
||||
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x01); out.write(0xf4);
|
||||
// Set 1 (APL/Text)
|
||||
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
// Programmed Symbols mode (3279 PS)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
||||
out.write(0x82); // flags: GE, CGCSGID present
|
||||
out.write(0x00); // more flags
|
||||
out.write(SW_3279_2); // SDW - default char width
|
||||
out.write(SH_3279_2); // SDH - default char height
|
||||
out.write(0x00); // Load PS format types supported: none
|
||||
out.write(SW_3279_2); // SDW (9)
|
||||
out.write(14); // SDH (14)
|
||||
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
|
||||
out.write(0x00); // Load PS device type (high)
|
||||
out.write(0x00); // Load PS device type (low)
|
||||
out.write(0x00); // reserved
|
||||
out.write(0x07); // DL = 7 bytes per descriptor (non-DBCS)
|
||||
out.write(0x07); // DL = 7 bytes per descriptor
|
||||
// Descriptor 1 (SET 0): default character set
|
||||
out.write(0x00); // SET 0
|
||||
out.write(0x10); // FLAGS: non-loadable, single-plane, single-byte, no compare
|
||||
out.write(0x00); // LCID 0
|
||||
out.write(0x02); // CGCSGID (4 bytes) = 0x02b90025 (CGEN|CSET)
|
||||
out.write(0xb9);
|
||||
out.write(0x00);
|
||||
out.write(0x25);
|
||||
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x01); out.write(0xf4);
|
||||
// Descriptor 2 (SET 1): APL/GE character set
|
||||
out.write(0x01); // SET 1
|
||||
out.write(0x00); // FLAGS: non-loadable, single-plane, single-byte, no compare
|
||||
out.write(0xf1); // LCID 0xf1
|
||||
out.write(0x03); // CGCSGID: 3179-style APL2 = 0x03c30136
|
||||
out.write(0xc3);
|
||||
out.write(0x01);
|
||||
out.write(0x36);
|
||||
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)
|
||||
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);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildColor() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(36);
|
||||
int colorMax = 16;
|
||||
out.write(0x00); // no options
|
||||
out.write(colorMax); // number of colors
|
||||
out.write(0x00); // default color pair: attribute
|
||||
out.write(0xf0 + HOST_COLOR_GREEN); // default color: green
|
||||
for (int i = 0xf1; i < 0xf1 + colorMax - 1; i++) {
|
||||
out.write(i); // color attribute value
|
||||
out.write(i); // maps to itself (color mode)
|
||||
}
|
||||
return out.toByteArray();
|
||||
// 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
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildHighlighting() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(11);
|
||||
out.write(5); // 5 pairs
|
||||
out.write(XAH_DEFAULT); out.write(XAH_NORMAL);
|
||||
out.write(XAH_BLINK); out.write(XAH_BLINK);
|
||||
out.write(XAH_REVERSE); out.write(XAH_REVERSE);
|
||||
out.write(XAH_UNDERSCORE); out.write(XAH_UNDERSCORE);
|
||||
out.write(XAH_INTENSIFY); out.write(XAH_INTENSIFY);
|
||||
return out.toByteArray();
|
||||
// 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
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildReplyModes() {
|
||||
@@ -265,21 +392,20 @@ public class QueryReplyBuilder {
|
||||
|
||||
private byte[] buildImplicitPartition(int maxCols, int maxRows) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
|
||||
// Implicit partition sizes SDP (Self-Defining Parameter)
|
||||
// Implicit partition sizes, 2 self-defining parameters
|
||||
|
||||
// SDP 1: Default screen size (Model 2: 80x24)
|
||||
out.write(0x00); // flags
|
||||
out.write(0x00); // reserved
|
||||
// SDP 1: Implicit partition sizes
|
||||
out.write(0x00); // SDP length high byte
|
||||
out.write(0x0b); // SDP length low byte (11 bytes: 2 len + 1 type + 1 res + 8 dims)
|
||||
out.write(0x0b); // SDP length (11 bytes)
|
||||
out.write(0x01); // SDP type: implicit partition sizes
|
||||
out.write(0x00); // reserved
|
||||
// Default dimensions (Model 2: 80x24)
|
||||
// Default size
|
||||
out.write((MODEL_2_COLS >> 8) & 0xFF);
|
||||
out.write(MODEL_2_COLS & 0xFF);
|
||||
out.write((MODEL_2_ROWS >> 8) & 0xFF);
|
||||
out.write(MODEL_2_ROWS & 0xFF);
|
||||
// Alternate dimensions
|
||||
// Alternate size (Model 4: 80x43, Model 5: 132x27, etc.)
|
||||
out.write((maxCols >> 8) & 0xFF);
|
||||
out.write(maxCols & 0xFF);
|
||||
out.write((maxRows >> 8) & 0xFF);
|
||||
@@ -287,4 +413,80 @@ public class QueryReplyBuilder {
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildGraphics() {
|
||||
return new byte[]{ (byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00 };
|
||||
}
|
||||
|
||||
private byte[] buildGImage() {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildAuxDev() {
|
||||
return new byte[]{
|
||||
0x00, 0x09, 0x00, 0x07,
|
||||
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
|
||||
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildSaveRestore() {
|
||||
// HOD DS3270.java line 1768: 6 bytes payload
|
||||
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
}
|
||||
|
||||
private byte[] buildTransparency() {
|
||||
// HOD DS3270.java line 1782: 2 bytes payload
|
||||
return new byte[]{ 0x00, 0x00 };
|
||||
}
|
||||
|
||||
private byte[] buildRpqNames() {
|
||||
// HOD DS3270.java line 1798: 5 bytes payload
|
||||
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
|
||||
}
|
||||
|
||||
private void appendOemFmt(ByteArrayOutputStream out) {
|
||||
// HOD DS3270.java line 1814: 4 distinct OEM format sub-fields
|
||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||
});
|
||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
|
||||
});
|
||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||
});
|
||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
|
||||
});
|
||||
}
|
||||
|
||||
private byte[] buildGColor() {
|
||||
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);
|
||||
java.awt.Color c = org.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
|
||||
out.write(c.getRed());
|
||||
out.write(c.getGreen());
|
||||
out.write(c.getBlue());
|
||||
out.write(0x00); // 6th byte in HOD color table
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildGSymbols() {
|
||||
return new byte[]{
|
||||
0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
|
||||
0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00,
|
||||
0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package org.lib3270j.graphics;
|
||||
|
||||
/**
|
||||
* Constants for IBM 3179G / 3270-PC GOCA (Graphics Object Content Architecture)
|
||||
* vector graphics and Programmed Symbols (PS).
|
||||
*/
|
||||
public final class GocaConstants {
|
||||
|
||||
private GocaConstants() {}
|
||||
|
||||
// Structured Field IDs
|
||||
public static final int SF_LOADPS = 0x0F; // 2-byte Structured Field prefix (or 0x06 for direct Load PS)
|
||||
public static final int SF_LOADPS_DIRECT = 0x06; // Load Programmed Symbols direct SFID
|
||||
public static final int SF_OBJCNTL = 0x24; // Object Control
|
||||
public static final int SF_OBJDATA = 0x85; // Graphics Object Data
|
||||
public static final int SF_3270_G = 0x20; // 3270 Graphics / Picture
|
||||
|
||||
// Structured Field Sub-IDs (for SF 0x0F)
|
||||
public static final int SF_LOADPS_SUB = 0x06; // Load Programmed Symbols
|
||||
public static final int SF_LOADLT_SUB = 0x07; // Load Line Type / Symbol Set
|
||||
public static final int SF_OBJDATA_SUB = 0x0F; // Graphics Object Data (GOCA draw orders)
|
||||
public static final int SF_OBJPICT_SUB = 0x10; // Graphics Object Picture (Segment draw orders)
|
||||
public static final int SF_OBJCNTL_SUB = 0x11; // Graphics Object Control (Procedure orders)
|
||||
|
||||
// Procedure Orders (for SF_OBJCNTL_SUB 0x11)
|
||||
public static final int P_NOP1 = 0x00; // Procedure NOOP
|
||||
public static final int P_COMT = 0x01; // Procedure Comment
|
||||
public static final int P_ATTCUR = 0x08; // Attach Graphic Cursor
|
||||
public static final int P_DETCUR = 0x09; // Detach Graphic Cursor
|
||||
public static final int P_ERASE = 0x0A; // Erase Graphics Presentation Space
|
||||
public static final int P_STOPDR = 0x0F; // Stop Draw
|
||||
public static final int P_SCUDEF = 0x21; // Set Current Defaults
|
||||
public static final int P_BEGPROC = 0x30; // Begin Procedure
|
||||
public static final int P_SETCUR = 0x31; // Set Graphic Cursor Position
|
||||
|
||||
// Coordinate space
|
||||
public static final int VIRTUAL_COORD_MAX = 4096;
|
||||
|
||||
// GOCA Drawing / Segment Orders
|
||||
public static final int G_NOP1 = 0x00; // NOOP 1-byte
|
||||
public static final int G_COMT = 0x01; // Comment
|
||||
public static final int G_GSMC = 0x07; // Set Marker Color
|
||||
public static final int G_GSPS = 0x08; // Set Pattern Set
|
||||
public static final int G_GSCOL = 0x0A; // Set Color
|
||||
public static final int G_GSMX = 0x0C; // Set Foreground Mix
|
||||
public static final int G_GSBMX = 0x0D; // Set Background Mix
|
||||
public static final int G_GSFLW = 0x11; // Set Fractional Line Width
|
||||
public static final int G_GSLT = 0x18; // Set Line Type
|
||||
public static final int G_GSLW = 0x19; // Set Line Width
|
||||
public static final int G_GSMS = 0x1B; // Set Marker Size
|
||||
public static final int G_GSCP = 0x21; // Set Current Position
|
||||
public static final int G_GSAP = 0x22; // Arc Parameters
|
||||
public static final int G_GSECOL = 0x26; // Set Extended Color
|
||||
public static final int G_GSVW = 0x27; // Set Viewing Window
|
||||
public static final int G_GSPT = 0x28; // Set Pattern Symbol
|
||||
public static final int G_GSMT = 0x29; // Set Marker Symbol / Type
|
||||
public static final int G_GSCH = 0x33; // Set Character Cell
|
||||
public static final int G_GSCA = 0x34; // Set Character Angle
|
||||
public static final int G_GSCR = 0x35; // Set Character Shear
|
||||
public static final int G_GSMCEL = 0x37; // Set Marker Cell
|
||||
public static final int G_GSCS = 0x38; // Set Character Set
|
||||
public static final int G_GSMP = 0x39; // Set Marker Precision
|
||||
public static final int G_GSCD = 0x3A; // Set Character Direction
|
||||
public static final int G_GSCC = 0x3B; // Set Character Precision
|
||||
public static final int G_GSMS_SET = 0x3C; // Set Marker Set
|
||||
public static final int G_ENDPROLOGUE = 0x3E; // End Prologue
|
||||
public static final int G_GPOP = 0x3F; // Pop Attribute
|
||||
public static final int G_GEAR = 0x60; // End Area
|
||||
public static final int G_GBAR = 0x68; // Begin Area
|
||||
public static final int G_BEGSEGM = 0x70; // Begin Segment
|
||||
public static final int G_ENDSEGM = 0x71; // End Segment
|
||||
public static final int G_GERASE = 0x7E; // Erase Graphics Plane
|
||||
public static final int G_GCLINE = 0x81; // Line at Current Position
|
||||
public static final int G_GCMRK = 0x82; // Marker at Current Position
|
||||
public static final int G_GCCHST = 0x83; // Character String at Current Position
|
||||
public static final int G_GCFLT = 0x85; // Fillet at Current Position
|
||||
public static final int G_GCARC = 0x86; // Partial Arc at Current Position
|
||||
public static final int G_GCFARC = 0x87; // Full Arc at Current Position
|
||||
public static final int G_GEIMG = 0x91; // End Image
|
||||
public static final int G_GIMD = 0x92; // Image Data
|
||||
public static final int G_GCRLIN = 0xA1; // Relative Line at Current Position
|
||||
public static final int G_GLINE = 0xC1; // Line (Absolute)
|
||||
public static final int G_GMRK = 0xC2; // Marker (Absolute)
|
||||
public static final int G_GCHST = 0xC3; // Character String (Absolute)
|
||||
public static final int G_GFLT = 0xC5; // Fillet (Absolute)
|
||||
public static final int G_GARC = 0xC6; // Partial Arc (Absolute)
|
||||
public static final int G_GFARC = 0xC7; // Full Arc (Absolute)
|
||||
public static final int G_GBIMG = 0xD1; // Begin Image
|
||||
public static final int G_GRLINE = 0xE1; // Relative Line (Absolute Start)
|
||||
|
||||
// Line Types (GSLT)
|
||||
public static final int LT_DEFAULT = 0;
|
||||
public static final int LT_DOT = 1;
|
||||
public static final int LT_SHORTDASH= 2;
|
||||
public static final int LT_DASHDOT = 3;
|
||||
public static final int LT_DOUBLEDOT= 4;
|
||||
public static final int LT_LONGDASH = 5;
|
||||
public static final int LT_DASHDOUBLEDOT = 6;
|
||||
public static final int LT_SOLID = 7;
|
||||
|
||||
// Line Widths (GSLW)
|
||||
public static final int LW_DEFAULT = 0;
|
||||
public static final int LW_NORMAL = 1;
|
||||
public static final int LW_THICK = 2;
|
||||
|
||||
// Fill Patterns (GSPT)
|
||||
public static final int PT_DEFAULT = 0;
|
||||
public static final int PT_D1 = 1;
|
||||
public static final int PT_D2 = 2;
|
||||
public static final int PT_D3 = 3;
|
||||
public static final int PT_D4 = 4;
|
||||
public static final int PT_D5 = 5;
|
||||
public static final int PT_D6 = 6;
|
||||
public static final int PT_D7 = 7;
|
||||
public static final int PT_D8 = 8;
|
||||
public static final int PT_VERT_LINE = 9;
|
||||
public static final int PT_HORIZ_LINE = 10;
|
||||
public static final int PT_DIAG_BLTR = 11;
|
||||
public static final int PT_DIAG_BLTR2 = 12;
|
||||
public static final int PT_DIAG_TLBR = 13;
|
||||
public static final int PT_DIAG_TLBR2 = 14;
|
||||
public static final int PT_EMPTY = 15;
|
||||
public static final int PT_SOLID = 16;
|
||||
|
||||
// Marker Symbols (GSMT)
|
||||
public static final int MK_DEFAULT = 0;
|
||||
public static final int MK_CROSS = 1; // x
|
||||
public static final int MK_PLUS = 2; // +
|
||||
public static final int MK_DIAMOND = 3; // <>
|
||||
public static final int MK_SQUARE = 4; // []
|
||||
public static final int MK_6STAR = 5; // * 6-point
|
||||
public static final int MK_8STAR = 6; // * 8-point
|
||||
public static final int MK_SDIAMOND = 7; // solid diamond
|
||||
public static final int MK_SSQUARE = 8; // solid square
|
||||
public static final int MK_DOT = 9; // .
|
||||
public static final int MK_CIRCLE = 10;// o
|
||||
|
||||
// Character Direction (GSCD)
|
||||
public static final int CD_DEFAULT = 0;
|
||||
public static final int CD_LR = 1; // Left to Right
|
||||
public static final int CD_TB = 2; // Top to Bottom
|
||||
public static final int CD_RL = 3; // Right to Left
|
||||
public static final int CD_BT = 4; // Bottom to Top
|
||||
|
||||
// Foreground / Background Mix Modes (GSMX / GSBMX)
|
||||
public static final int MIX_DEFAULT = 0;
|
||||
public static final int MIX_OR = 1;
|
||||
public static final int MIX_OVER = 2;
|
||||
public static final int MIX_LEAVE = 3;
|
||||
public static final int MIX_XOR = 4;
|
||||
public static final int MIX_UNDER = 5;
|
||||
|
||||
// Graphic Colors (IBM 3179G / HOD 16-color table)
|
||||
public static final java.awt.Color[] GOCA_COLORS = new java.awt.Color[] {
|
||||
new java.awt.Color(0, 255, 0), // 0: Default (Green)
|
||||
new java.awt.Color(120, 144, 240), // 1: Blue
|
||||
new java.awt.Color(255, 0, 0), // 2: Red
|
||||
new java.awt.Color(255, 0, 255), // 3: Pink / Magenta
|
||||
new java.awt.Color(0, 255, 0), // 4: Green
|
||||
new java.awt.Color(0, 255, 255), // 5: Turquoise / Cyan
|
||||
new java.awt.Color(255, 255, 0), // 6: Yellow
|
||||
new java.awt.Color(255, 255, 255), // 7: Neutral White
|
||||
new java.awt.Color(0, 0, 0), // 8: Black
|
||||
new java.awt.Color(0, 0, 128), // 9: Deep Blue
|
||||
new java.awt.Color(128, 0, 0), // 10: Orange / Dark Red
|
||||
new java.awt.Color(128, 0, 128), // 11: Purple
|
||||
new java.awt.Color(0, 128, 0), // 12: Pale Green
|
||||
new java.awt.Color(0, 128, 128), // 13: Pale Cyan
|
||||
new java.awt.Color(215, 151, 0), // 14: Mustard
|
||||
new java.awt.Color(192, 192, 192), // 15: Grey / Light White
|
||||
new java.awt.Color(73, 36, 0) // 16: Brown
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,819 @@
|
||||
package org.lib3270j.graphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
|
||||
/**
|
||||
* Interprets GOCA (Graphics Object Content Architecture) drawing orders
|
||||
* and updates the GraphicsPlane.
|
||||
*/
|
||||
public class GocaDecoder {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(GocaDecoder.class.getName());
|
||||
|
||||
private final GraphicsPlane plane;
|
||||
|
||||
// Drawing state
|
||||
private int curX = 0;
|
||||
private int curY = 0;
|
||||
private Color curColor = Color.GREEN;
|
||||
private int lineType = GocaConstants.LT_SOLID;
|
||||
private int lineWidth = GocaConstants.LW_NORMAL;
|
||||
private int markerType = GocaConstants.MK_PLUS;
|
||||
private int markerSize = 5;
|
||||
private Color markerColor = Color.GREEN;
|
||||
private int pattern = GocaConstants.PT_SOLID;
|
||||
private Color fillColor = Color.GREEN;
|
||||
private int charDir = GocaConstants.CD_LR;
|
||||
private double charAngle = 0.0;
|
||||
private int charWidth = 9;
|
||||
private int charHeight = 16;
|
||||
private int charSet = 0;
|
||||
private int arcParamP = 1;
|
||||
private int arcParamQ = 1;
|
||||
private int arcParamR = 0;
|
||||
private int arcParamS = 0;
|
||||
|
||||
private ProgramSymbolManager programSymbolManager;
|
||||
|
||||
// Area accumulation
|
||||
private boolean inArea = false;
|
||||
private boolean areaDrawBoundary = true;
|
||||
private final List<Integer> areaPointsX = new ArrayList<>();
|
||||
private final List<Integer> areaPointsY = new ArrayList<>();
|
||||
|
||||
// Image accumulation
|
||||
private boolean inImage = false;
|
||||
private int imgX = 0;
|
||||
private int imgY = 0;
|
||||
private int imgWidth = 0;
|
||||
private int imgHeight = 0;
|
||||
private final List<Byte> imgBuffer = new ArrayList<>();
|
||||
|
||||
public GocaDecoder(GraphicsPlane plane) {
|
||||
this.plane = plane;
|
||||
}
|
||||
|
||||
public void setProgramSymbolManager(ProgramSymbolManager psm) {
|
||||
this.programSymbolManager = psm;
|
||||
}
|
||||
|
||||
public GraphicsPlane getGraphicsPlane() {
|
||||
return plane;
|
||||
}
|
||||
|
||||
public int getCurX() {
|
||||
return curX;
|
||||
}
|
||||
|
||||
public int getCurY() {
|
||||
return curY;
|
||||
}
|
||||
|
||||
public synchronized void resetDefaults() {
|
||||
curX = 0;
|
||||
curY = 0;
|
||||
resetAttributes();
|
||||
}
|
||||
|
||||
public synchronized void resetAttributes() {
|
||||
curColor = getColor(0);
|
||||
lineType = GocaConstants.LT_SOLID;
|
||||
lineWidth = GocaConstants.LW_NORMAL;
|
||||
markerType = GocaConstants.MK_PLUS;
|
||||
markerSize = 5;
|
||||
markerColor = curColor;
|
||||
pattern = GocaConstants.PT_SOLID;
|
||||
fillColor = curColor;
|
||||
charDir = GocaConstants.CD_LR;
|
||||
charAngle = 0.0;
|
||||
charSet = 0;
|
||||
inArea = false;
|
||||
areaPointsX.clear();
|
||||
areaPointsY.clear();
|
||||
inImage = false;
|
||||
imgBuffer.clear();
|
||||
}
|
||||
|
||||
private byte[] partialOrderBuffer = new byte[0];
|
||||
|
||||
private int getOrderLength(byte[] data, int idx, int end) {
|
||||
int order = data[idx] & 0xFF;
|
||||
if (order == GocaConstants.G_NOP1 || order == 0xFF) {
|
||||
return 1;
|
||||
}
|
||||
if (order == GocaConstants.G_GEAR ||
|
||||
order == GocaConstants.G_ENDSEGM || order == GocaConstants.G_ENDPROLOGUE ||
|
||||
order == GocaConstants.G_GEIMG || order == GocaConstants.G_GPOP) {
|
||||
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
|
||||
}
|
||||
if (order == 0x04 || order == GocaConstants.G_GSMC || order == GocaConstants.G_GSPS ||
|
||||
order == GocaConstants.G_GSCOL || order == GocaConstants.G_GSMX ||
|
||||
order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSFLW ||
|
||||
order == GocaConstants.G_GSLT || order == GocaConstants.G_GSLW ||
|
||||
order == GocaConstants.G_GSMS || order == GocaConstants.G_GSPT ||
|
||||
order == GocaConstants.G_GSMT || order == GocaConstants.G_GSCS ||
|
||||
order == GocaConstants.G_GSMP || order == GocaConstants.G_GSCD ||
|
||||
order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMS_SET ||
|
||||
order == GocaConstants.G_GBAR) {
|
||||
return 2;
|
||||
}
|
||||
if (order == GocaConstants.G_GSAP || order == GocaConstants.G_GBIMG || order == 0x91) {
|
||||
return 10;
|
||||
}
|
||||
if (idx + 1 >= end) {
|
||||
return -1;
|
||||
}
|
||||
return (data[idx + 1] & 0xFF) + 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a stream of GOCA drawing orders.
|
||||
*/
|
||||
public synchronized void decodeStream(byte[] data, int offset, int length) {
|
||||
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] inputData;
|
||||
int idx;
|
||||
int end;
|
||||
|
||||
if (partialOrderBuffer.length > 0) {
|
||||
inputData = new byte[partialOrderBuffer.length + length];
|
||||
System.arraycopy(partialOrderBuffer, 0, inputData, 0, partialOrderBuffer.length);
|
||||
System.arraycopy(data, offset, inputData, partialOrderBuffer.length, length);
|
||||
idx = 0;
|
||||
end = inputData.length;
|
||||
partialOrderBuffer = new byte[0];
|
||||
} else {
|
||||
inputData = data;
|
||||
idx = offset;
|
||||
end = offset + length;
|
||||
}
|
||||
|
||||
while (idx < end) {
|
||||
int order = inputData[idx] & 0xFF;
|
||||
int orderLen = getOrderLength(inputData, idx, end);
|
||||
|
||||
if (orderLen == -1 || idx + orderLen > end) {
|
||||
int remaining = end - idx;
|
||||
partialOrderBuffer = new byte[remaining];
|
||||
System.arraycopy(inputData, idx, partialOrderBuffer, 0, remaining);
|
||||
break;
|
||||
}
|
||||
|
||||
int payloadLen = (orderLen >= 2) ? (inputData[idx + 1] & 0xFF) : 0;
|
||||
|
||||
switch (order) {
|
||||
case GocaConstants.G_NOP1:
|
||||
case 0xFF: {
|
||||
idx++;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GEAR: {
|
||||
endArea();
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GEIMG: {
|
||||
endImage();
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70)
|
||||
if (idx + 7 < end && (inputData[idx + 7] & 0x06) == 0) {
|
||||
resetAttributes();
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_ENDSEGM: { // End Segment (0x71)
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_ENDPROLOGUE: { // End Prologue (0x3E)
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_COMT: { // Comment (0x01)
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSCP: { // Set Current Position (0x21)
|
||||
if (payloadLen >= 4 && idx + 5 < end) {
|
||||
curX = readCoord(inputData, idx + 2);
|
||||
curY = readCoord(inputData, idx + 4);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSAP: { // Arc Parameters (0x22)
|
||||
if (idx + 9 < end) {
|
||||
arcParamP = readCoord(inputData, idx + 2);
|
||||
arcParamQ = readCoord(inputData, idx + 4);
|
||||
arcParamR = readCoord(inputData, idx + 6);
|
||||
arcParamS = readCoord(inputData, idx + 8);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSVW: { // Set Viewing Window (0x27)
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSCA: { // Set Character Angle (0x34)
|
||||
if (payloadLen >= 4 && idx + 5 < end) {
|
||||
int ax = readCoord(inputData, idx + 2);
|
||||
int ay = readCoord(inputData, idx + 4);
|
||||
if (ax != 0 || ay != 0) {
|
||||
charAngle = Math.toDegrees(Math.atan2(ay, ax));
|
||||
}
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSCH: { // Set Character Cell (0x33)
|
||||
if (payloadLen >= 4 && idx + 5 < end) {
|
||||
charWidth = readCoord(inputData, idx + 2);
|
||||
charHeight = readCoord(inputData, idx + 4);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSCR: { // Set Character Shear (0x35)
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSCOL: { // Set Color (0x0A)
|
||||
int colIdx = inputData[idx + 1] & 0xFF;
|
||||
curColor = getColor(colIdx);
|
||||
fillColor = curColor;
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSECOL: { // Set Extended Color (0x26)
|
||||
if (payloadLen >= 2 && idx + 3 < end) {
|
||||
int colIdx = inputData[idx + 3] & 0xFF;
|
||||
curColor = getColor(colIdx);
|
||||
fillColor = curColor;
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSLT: { // Set Line Type (0x18)
|
||||
lineType = inputData[idx + 1] & 0xFF;
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSLW: { // Set Line Width (0x19)
|
||||
lineWidth = inputData[idx + 1] & 0xFF;
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSMC: { // Set Marker Color (0x07)
|
||||
int colIdx = inputData[idx + 1] & 0xFF;
|
||||
markerColor = getColor(colIdx);
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSMS: { // Set Marker Size (0x1B)
|
||||
markerSize = inputData[idx + 1] & 0xFF;
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSMT: { // Set Marker Type (0x29)
|
||||
markerType = inputData[idx + 1] & 0xFF;
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSPS: { // Set Pattern Set (0x08)
|
||||
pattern = inputData[idx + 1] & 0xFF;
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSPT: { // Set Pattern Symbol (0x28)
|
||||
pattern = inputData[idx + 1] & 0xFF;
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSCD: { // Set Character Direction (0x3A)
|
||||
charDir = inputData[idx + 1] & 0xFF;
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSCS: { // Set Character Set (0x38)
|
||||
charSet = inputData[idx + 1] & 0xFF;
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case 0x04:
|
||||
case GocaConstants.G_GSMX:
|
||||
case GocaConstants.G_GSBMX:
|
||||
case GocaConstants.G_GSFLW:
|
||||
case GocaConstants.G_GSMP:
|
||||
case GocaConstants.G_GSCC:
|
||||
case GocaConstants.G_GSMS_SET:
|
||||
case GocaConstants.G_GPOP: {
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GBAR: { // Begin Area (0x68)
|
||||
int flags = inputData[idx + 1] & 0xFF;
|
||||
beginArea((flags & 0x40) != 0);
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GLINE: { // Line Absolute (0xC1)
|
||||
if (payloadLen >= 4) {
|
||||
processLine(inputData, idx + 2, payloadLen, false);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GCLINE: { // Line Current Position (0x81)
|
||||
if (payloadLen >= 4) {
|
||||
processLine(inputData, idx + 2, payloadLen, true);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GRLINE: { // Relative Line Absolute Start (0xE1)
|
||||
if (payloadLen >= 4) {
|
||||
processRelativeLine(inputData, idx + 2, payloadLen, false);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GCRLIN: { // Relative Line Current Position (0xA1)
|
||||
if (payloadLen >= 2) {
|
||||
processRelativeLine(inputData, idx + 2, payloadLen, true);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GFARC: { // Full Arc Absolute (0xC7)
|
||||
if (payloadLen >= 4) {
|
||||
processArc(inputData, idx + 2, payloadLen, false, true);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GCFARC: { // Full Arc Current Position (0x87)
|
||||
if (payloadLen >= 2) {
|
||||
processArc(inputData, idx + 2, payloadLen, true, true);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GARC: { // Partial Arc Absolute (0xC6)
|
||||
if (payloadLen >= 8) {
|
||||
processArc(inputData, idx + 2, payloadLen, false, false);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GCARC: { // Partial Arc Current Position (0x86)
|
||||
if (payloadLen >= 4) {
|
||||
processArc(inputData, idx + 2, payloadLen, true, false);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GFLT: { // Fillet Absolute (0xC5)
|
||||
if (payloadLen >= 4) {
|
||||
processFillet(inputData, idx + 2, payloadLen, false);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GCFLT: { // Fillet Current Position (0x85)
|
||||
if (payloadLen >= 4) {
|
||||
processFillet(inputData, idx + 2, payloadLen, true);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GMRK: { // Marker Absolute (0xC2)
|
||||
if (payloadLen >= 4) {
|
||||
processMarker(inputData, idx + 2, payloadLen, false);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GCMRK: { // Marker Current Position (0x82)
|
||||
if (payloadLen >= 4) {
|
||||
processMarker(inputData, idx + 2, payloadLen, true);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GCHST: { // Character String Absolute (0xC3)
|
||||
if (payloadLen >= 4) {
|
||||
processText(inputData, idx + 2, payloadLen, false);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GCCHST: { // Character String Current Position (0x83)
|
||||
if (payloadLen >= 0) {
|
||||
processText(inputData, idx + 2, payloadLen, true);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GBIMG: { // Begin Image (0xD1)
|
||||
if (idx + 9 < 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);
|
||||
beginImage(x, y, w, h);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GIMD: { // Image Data (0x92)
|
||||
if (inImage) {
|
||||
for (int k = 0; k < payloadLen; k++) {
|
||||
imgBuffer.add(inputData[idx + 2 + k]);
|
||||
}
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a stream of Object Control Procedure Orders (subtype 0x11).
|
||||
*/
|
||||
public synchronized void processProcedureOrders(byte[] data, int offset, int length) {
|
||||
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
int idx = offset;
|
||||
int end = offset + length;
|
||||
|
||||
while (idx < end) {
|
||||
int order = data[idx] & 0xFF;
|
||||
|
||||
switch (order) {
|
||||
case GocaConstants.P_NOP1:
|
||||
case GocaConstants.P_ATTCUR:
|
||||
case GocaConstants.P_DETCUR:
|
||||
case GocaConstants.P_STOPDR: {
|
||||
idx++;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space
|
||||
plane.clear();
|
||||
resetDefaults();
|
||||
idx++;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.P_BEGPROC: { // 0x30: Begin Procedure (length 12)
|
||||
int len = 12;
|
||||
if (idx + 1 < end && data[idx + 1] != 0) {
|
||||
len = (data[idx + 1] & 0xFF) + 2;
|
||||
}
|
||||
idx += len;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.P_SCUDEF: { // 0x21: Set Current Defaults
|
||||
if (idx + 1 < end) {
|
||||
int len = data[idx + 1] & 0xFF;
|
||||
if (idx + 2 + len <= end) {
|
||||
decodeStream(data, idx + 2, len);
|
||||
}
|
||||
idx += 2 + len;
|
||||
} else {
|
||||
idx++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GocaConstants.P_COMT:
|
||||
case GocaConstants.P_SETCUR: {
|
||||
if (idx + 1 < end) {
|
||||
int len = data[idx + 1] & 0xFF;
|
||||
idx += 2 + len;
|
||||
} else {
|
||||
idx++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (idx + 1 < end) {
|
||||
int len = data[idx + 1] & 0xFF;
|
||||
idx += 2 + len;
|
||||
} else {
|
||||
idx++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void beginArea(boolean drawBoundary) {
|
||||
this.inArea = true;
|
||||
this.areaDrawBoundary = drawBoundary;
|
||||
this.fillColor = this.curColor;
|
||||
this.areaPointsX.clear();
|
||||
this.areaPointsY.clear();
|
||||
}
|
||||
|
||||
private void endArea() {
|
||||
if (!inArea || areaPointsX.size() < 3) {
|
||||
inArea = false;
|
||||
areaPointsX.clear();
|
||||
areaPointsY.clear();
|
||||
return;
|
||||
}
|
||||
int n = areaPointsX.size();
|
||||
int[] px = new int[n];
|
||||
int[] py = new int[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
px[i] = plane.mapX(areaPointsX.get(i));
|
||||
py[i] = plane.mapY(areaPointsY.get(i));
|
||||
}
|
||||
|
||||
plane.fillArea(px, py, n, fillColor, pattern, areaDrawBoundary, curColor, lineType, lineWidth);
|
||||
inArea = false;
|
||||
areaPointsX.clear();
|
||||
areaPointsY.clear();
|
||||
}
|
||||
|
||||
private void addAreaPoint(int x, int y) {
|
||||
if (inArea) {
|
||||
areaPointsX.add(x);
|
||||
areaPointsY.add(y);
|
||||
}
|
||||
}
|
||||
|
||||
private void beginImage(int x, int y, int w, int h) {
|
||||
this.inImage = true;
|
||||
this.imgX = x;
|
||||
this.imgY = y;
|
||||
this.imgWidth = w;
|
||||
this.imgHeight = h;
|
||||
this.imgBuffer.clear();
|
||||
}
|
||||
|
||||
private void endImage() {
|
||||
if (!inImage || imgWidth <= 0 || imgHeight <= 0 || imgBuffer.isEmpty()) {
|
||||
inImage = false;
|
||||
return;
|
||||
}
|
||||
byte[] bytes = new byte[imgBuffer.size()];
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = imgBuffer.get(i);
|
||||
}
|
||||
|
||||
int px = plane.mapX(imgX);
|
||||
int py = plane.mapY(imgY);
|
||||
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor);
|
||||
inImage = false;
|
||||
imgBuffer.clear();
|
||||
}
|
||||
|
||||
private void processLine(byte[] data, int off, int len, boolean fromCurPos) {
|
||||
int pos = off;
|
||||
int end = off + len;
|
||||
|
||||
int startX = curX;
|
||||
int startY = curY;
|
||||
|
||||
if (!fromCurPos && pos + 4 <= end) {
|
||||
startX = readCoord(data, pos);
|
||||
startY = readCoord(data, pos + 2);
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
if (inArea) {
|
||||
addAreaPoint(startX, startY);
|
||||
}
|
||||
|
||||
while (pos + 4 <= end) {
|
||||
int nextX = readCoord(data, pos);
|
||||
int nextY = readCoord(data, pos + 2);
|
||||
pos += 4;
|
||||
|
||||
if (inArea) {
|
||||
addAreaPoint(nextX, nextY);
|
||||
} else {
|
||||
plane.drawLine(plane.mapX(startX), plane.mapY(startY),
|
||||
plane.mapX(nextX), plane.mapY(nextY),
|
||||
curColor, lineType, lineWidth);
|
||||
}
|
||||
|
||||
startX = nextX;
|
||||
startY = nextY;
|
||||
}
|
||||
|
||||
curX = startX;
|
||||
curY = startY;
|
||||
}
|
||||
|
||||
private void processRelativeLine(byte[] data, int off, int len, boolean fromCurPos) {
|
||||
int pos = off;
|
||||
int end = off + len;
|
||||
|
||||
int startX = curX;
|
||||
int startY = curY;
|
||||
|
||||
if (!fromCurPos && pos + 4 <= end) {
|
||||
startX = readCoord(data, pos);
|
||||
startY = readCoord(data, pos + 2);
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
if (inArea) {
|
||||
addAreaPoint(startX, startY);
|
||||
}
|
||||
|
||||
while (pos + 2 <= end) {
|
||||
int dx = (byte) data[pos];
|
||||
int dy = (byte) data[pos + 1];
|
||||
pos += 2;
|
||||
|
||||
int nextX = startX + dx;
|
||||
int nextY = startY + dy;
|
||||
|
||||
if (inArea) {
|
||||
addAreaPoint(nextX, nextY);
|
||||
} else {
|
||||
plane.drawLine(plane.mapX(startX), plane.mapY(startY),
|
||||
plane.mapX(nextX), plane.mapY(nextY),
|
||||
curColor, lineType, lineWidth);
|
||||
}
|
||||
|
||||
startX = nextX;
|
||||
startY = nextY;
|
||||
}
|
||||
|
||||
curX = startX;
|
||||
curY = startY;
|
||||
}
|
||||
|
||||
private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) {
|
||||
int pos = off;
|
||||
int startX = curX;
|
||||
int startY = curY;
|
||||
|
||||
if (!fromCurPos && pos + 4 <= off + len) {
|
||||
startX = readCoord(data, pos);
|
||||
startY = readCoord(data, pos + 2);
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
double multiplier = 1.0;
|
||||
if (pos + 2 <= off + len) {
|
||||
multiplier = (data[pos] & 0xFF) + ((data[pos + 1] & 0xFF) / 255.0);
|
||||
if (multiplier == 0.0) multiplier = 1.0;
|
||||
}
|
||||
|
||||
int dxP = Math.abs(arcParamP - arcParamR);
|
||||
int dyQ = Math.abs(arcParamQ - arcParamS);
|
||||
if (dxP == 0) dxP = 10;
|
||||
if (dyQ == 0) dyQ = 10;
|
||||
|
||||
int rxVirtual = (int) (dxP * multiplier);
|
||||
int ryVirtual = (int) (dyQ * multiplier);
|
||||
|
||||
int rx = Math.abs(plane.mapX(rxVirtual) - plane.mapX(0));
|
||||
int ry = Math.abs(plane.mapY(ryVirtual) - plane.mapY(0));
|
||||
if (rx <= 0) rx = 10;
|
||||
if (ry <= 0) ry = 10;
|
||||
|
||||
plane.drawArc(plane.mapX(startX), plane.mapY(startY), rx, ry, 0.0, 360.0,
|
||||
curColor, lineType, lineWidth, isFull);
|
||||
|
||||
curX = startX;
|
||||
curY = startY;
|
||||
}
|
||||
|
||||
private void processFillet(byte[] data, int off, int len, boolean fromCurPos) {
|
||||
int pos = off;
|
||||
int end = off + len;
|
||||
|
||||
List<Integer> ptsX = new ArrayList<>();
|
||||
List<Integer> ptsY = new ArrayList<>();
|
||||
|
||||
if (fromCurPos) {
|
||||
ptsX.add(plane.mapX(curX));
|
||||
ptsY.add(plane.mapY(curY));
|
||||
}
|
||||
|
||||
while (pos + 4 <= end) {
|
||||
int x = readCoord(data, pos);
|
||||
int y = readCoord(data, pos + 2);
|
||||
ptsX.add(plane.mapX(x));
|
||||
ptsY.add(plane.mapY(y));
|
||||
curX = x;
|
||||
curY = y;
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
if (ptsX.size() >= 2) {
|
||||
int n = ptsX.size();
|
||||
int[] px = new int[n];
|
||||
int[] py = new int[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
px[i] = ptsX.get(i);
|
||||
py[i] = ptsY.get(i);
|
||||
}
|
||||
plane.drawFillet(px, py, n, curColor, lineType, lineWidth);
|
||||
}
|
||||
}
|
||||
|
||||
private void processMarker(byte[] data, int off, int len, boolean fromCurPos) {
|
||||
int pos = off;
|
||||
int end = off + len;
|
||||
|
||||
if (fromCurPos) {
|
||||
plane.drawMarker(plane.mapX(curX), plane.mapY(curY), markerType, markerSize, markerColor);
|
||||
}
|
||||
|
||||
while (pos + 4 <= end) {
|
||||
int x = readCoord(data, pos);
|
||||
int y = readCoord(data, pos + 2);
|
||||
plane.drawMarker(plane.mapX(x), plane.mapY(y), markerType, markerSize, markerColor);
|
||||
curX = x;
|
||||
curY = y;
|
||||
pos += 4;
|
||||
}
|
||||
}
|
||||
|
||||
private void processText(byte[] data, int off, int len, boolean fromCurPos) {
|
||||
int pos = off;
|
||||
int end = off + len;
|
||||
|
||||
int startX = curX;
|
||||
int startY = curY;
|
||||
|
||||
if (!fromCurPos && pos + 4 <= end) {
|
||||
startX = readCoord(data, pos);
|
||||
startY = readCoord(data, pos + 2);
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
int textLen = end - pos;
|
||||
if (textLen <= 0) return;
|
||||
|
||||
int cw = charWidth > 0 ? (int) Math.round((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 12;
|
||||
int ch = charHeight > 0 ? (int) Math.round((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 20;
|
||||
|
||||
if (charSet != 0 && programSymbolManager != null) {
|
||||
for (int i = 0; i < textLen; i++) {
|
||||
int code = data[pos + i] & 0xFF;
|
||||
int px = plane.mapX(startX);
|
||||
int py = plane.mapY(startY) - ch;
|
||||
boolean drawn = programSymbolManager.drawSymbol(plane.getGraphics(), charSet, code, px, py, cw, ch, curColor, null);
|
||||
if (!drawn) {
|
||||
char c = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
|
||||
plane.drawVectorText(px, py, String.valueOf(c), curColor, cw, ch, charDir, charAngle);
|
||||
}
|
||||
startX += (charWidth > 0 ? charWidth : 9);
|
||||
}
|
||||
curX = startX;
|
||||
curY = startY;
|
||||
return;
|
||||
}
|
||||
|
||||
char[] chars = new char[textLen];
|
||||
for (int i = 0; i < textLen; i++) {
|
||||
chars[i] = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
|
||||
}
|
||||
String text = new String(chars);
|
||||
|
||||
plane.drawVectorText(plane.mapX(startX), plane.mapY(startY) - ch, text,
|
||||
curColor, cw, ch, charDir, charAngle);
|
||||
|
||||
curX = startX + (textLen * (charWidth > 0 ? charWidth : 9));
|
||||
curY = startY;
|
||||
}
|
||||
|
||||
private int readCoord(byte[] data, int off) {
|
||||
return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF));
|
||||
}
|
||||
|
||||
private Color getColor(int colorIndex) {
|
||||
if (colorIndex >= 0 && colorIndex < GocaConstants.GOCA_COLORS.length) {
|
||||
return GocaConstants.GOCA_COLORS[colorIndex];
|
||||
}
|
||||
return Color.GREEN;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.lib3270j.graphics;
|
||||
|
||||
/**
|
||||
* Graphics modes supported by j3270 / lib3270j.
|
||||
*/
|
||||
public enum GraphicsMode {
|
||||
/** Text only (no graphics query replies, classic 3279-4 terminal behavior). */
|
||||
NONE("None (Text Only)"),
|
||||
|
||||
/** Programmed Symbols only (custom character matrices and APL, single & triple plane). */
|
||||
PROGRAMMED_SYMBOLS("Programmed Symbols Only"),
|
||||
|
||||
/** Vector graphics only (GOCA / 3179G drawing orders). */
|
||||
VECTOR_GRAPHICS("Vector Graphics Only"),
|
||||
|
||||
/** Full graphics support (both Programmed Symbols and Vector Graphics). */
|
||||
BOTH("Both (Programmed Symbols & Vector Graphics)");
|
||||
|
||||
private final String description;
|
||||
|
||||
GraphicsMode(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public boolean isProgrammedSymbolsEnabled() {
|
||||
return this == PROGRAMMED_SYMBOLS || this == BOTH;
|
||||
}
|
||||
|
||||
public boolean isVectorGraphicsEnabled() {
|
||||
return this == VECTOR_GRAPHICS || this == BOTH;
|
||||
}
|
||||
|
||||
public static GraphicsMode fromString(String str) {
|
||||
if (str == null || str.trim().isEmpty()) {
|
||||
return NONE;
|
||||
}
|
||||
String s = str.trim().toUpperCase();
|
||||
switch (s) {
|
||||
case "BOTH":
|
||||
case "ALL":
|
||||
case "FULL":
|
||||
case "ON":
|
||||
case "TRUE":
|
||||
return BOTH;
|
||||
case "PS":
|
||||
case "PROGRAMMED_SYMBOLS":
|
||||
case "PROGRAMMEDSYMBOLS":
|
||||
case "SYMBOLS":
|
||||
case "APL":
|
||||
return PROGRAMMED_SYMBOLS;
|
||||
case "VECTOR":
|
||||
case "VECTOR_GRAPHICS":
|
||||
case "VECTORGRAPHICS":
|
||||
case "GOCA":
|
||||
case "GDDM":
|
||||
return VECTOR_GRAPHICS;
|
||||
case "NONE":
|
||||
case "OFF":
|
||||
case "FALSE":
|
||||
case "DISABLED":
|
||||
case "TEXT":
|
||||
default:
|
||||
return NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package org.lib3270j.graphics;
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Polygon;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.Stroke;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Arc2D;
|
||||
import java.awt.geom.GeneralPath;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Offscreen rendering surface for GOCA vector graphics.
|
||||
* Maintained as an ARGB BufferedImage that overlays the 3270 character cell matrix.
|
||||
*/
|
||||
public class GraphicsPlane {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(GraphicsPlane.class.getName());
|
||||
|
||||
// 17 Standard GOCA 8x8 Fill Patterns
|
||||
public static final byte[][] PATTERN_DATA = new byte[][]{
|
||||
{-1, -1, -1, -1, -1, -1, -1, -1}, // 0: Solid (all 1s)
|
||||
{-1, -1, -1, -18, -1, -1, -1, -18}, // 1: D1
|
||||
{-1, -69, -1, -18, -1, -69, -1, -18}, // 2: D2
|
||||
{119, -35, -69, -18, 119, -35, -69, -18}, // 3: D3
|
||||
{-69, -52, 51, -18, -69, -52, 51, -18}, // 4: D4
|
||||
{85, -86, 85, -86, 85, -86, 85, -86}, // 5: D5 (50% checker)
|
||||
{68, 51, -52, 17, 68, 51, -52, 17}, // 6: D6
|
||||
{-120, 34, 68, 17, -120, 34, 68, 17}, // 7: D7
|
||||
{0, 68, 0, 17, 0, 68, 0, 17}, // 8: D8 (sparse dots)
|
||||
{-128, -128, -128, -128, -128, -128, -128, -128}, // 9: Vertical line
|
||||
{-1, 0, 0, 0, 0, 0, 0, 0}, // 10: Horizontal line
|
||||
{1, 2, 4, 8, 16, 32, 64, -128}, // 11: Diagonal bottom-left to top-right
|
||||
{3, 12, 48, -64, 3, 12, 48, -64}, // 12: Diagonal BL-TR dense
|
||||
{-128, 64, 32, 16, 8, 4, 2, 1}, // 13: Diagonal top-left to bottom-right
|
||||
{-64, 48, 12, 3, -64, 48, 12, 3}, // 14: Diagonal TL-BR dense
|
||||
{0, 0, 0, 0, 0, 0, 0, 0}, // 15: Empty (transparent)
|
||||
{-1, -1, -1, -1, -1, -1, -1, -1} // 16: Solid
|
||||
};
|
||||
|
||||
private int canvasWidth = 800;
|
||||
private int canvasHeight = 600;
|
||||
private BufferedImage image;
|
||||
private Graphics2D g2d;
|
||||
private boolean hasContent = false;
|
||||
|
||||
public GraphicsPlane(int width, int height) {
|
||||
resize(width, height);
|
||||
}
|
||||
|
||||
public synchronized void resize(int width, int height) {
|
||||
int w = Math.max(1, width);
|
||||
int h = Math.max(1, height);
|
||||
if (w == canvasWidth && h == canvasHeight && image != null) {
|
||||
return;
|
||||
}
|
||||
this.canvasWidth = w;
|
||||
this.canvasHeight = h;
|
||||
|
||||
BufferedImage newImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D newG2d = newImage.createGraphics();
|
||||
newG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
newG2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
|
||||
if (image != null && hasContent) {
|
||||
newG2d.drawImage(image, 0, 0, w, h, null);
|
||||
}
|
||||
|
||||
if (this.g2d != null) {
|
||||
this.g2d.dispose();
|
||||
}
|
||||
|
||||
this.image = newImage;
|
||||
this.g2d = newG2d;
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
if (image != null && g2d != null) {
|
||||
BufferedImage newImage = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D newG2d = newImage.createGraphics();
|
||||
newG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
if (this.g2d != null) {
|
||||
this.g2d.dispose();
|
||||
}
|
||||
this.image = newImage;
|
||||
this.g2d = newG2d;
|
||||
}
|
||||
hasContent = false;
|
||||
}
|
||||
|
||||
public synchronized boolean hasContent() {
|
||||
return hasContent;
|
||||
}
|
||||
|
||||
public synchronized BufferedImage getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public int getCanvasWidth() {
|
||||
return canvasWidth;
|
||||
}
|
||||
|
||||
public int getCanvasHeight() {
|
||||
return canvasHeight;
|
||||
}
|
||||
|
||||
public synchronized Graphics2D getGraphics() {
|
||||
return g2d;
|
||||
}
|
||||
|
||||
private int screenCols = 80;
|
||||
private int screenRows = 24;
|
||||
|
||||
public void setScreenDimensions(int cols, int rows) {
|
||||
this.screenCols = cols > 0 ? cols : 80;
|
||||
this.screenRows = rows > 0 ? rows : 24;
|
||||
}
|
||||
|
||||
public int getScreenCols() {
|
||||
return screenCols;
|
||||
}
|
||||
|
||||
public int getScreenRows() {
|
||||
return screenRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X.
|
||||
*/
|
||||
public int mapX(int gocaX) {
|
||||
int nominalWidth = screenCols * 9;
|
||||
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
|
||||
int nx = gocaX + xMax;
|
||||
return (int) Math.round((double) nx * canvasWidth / nominalWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
|
||||
*/
|
||||
public int mapY(int gocaY) {
|
||||
int nominalHeight = screenRows * 16;
|
||||
int yMax = (nominalHeight - 1) / 2;
|
||||
int ny = yMax - gocaY;
|
||||
return (int) Math.round((double) ny * canvasHeight / nominalHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an absolute or relative line.
|
||||
*/
|
||||
public synchronized void drawLine(int x1, int y1, int x2, int y2, Color color, int lineType, int lineWidth) {
|
||||
if (g2d == null) return;
|
||||
g2d.setColor(color != null ? color : Color.WHITE);
|
||||
g2d.setStroke(createStroke(lineType, lineWidth));
|
||||
g2d.drawLine(x1, y1, x2, y2);
|
||||
hasContent = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a full or partial arc / ellipse.
|
||||
*/
|
||||
public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg,
|
||||
Color color, int lineType, int lineWidth, boolean isFull) {
|
||||
if (g2d == null) return;
|
||||
g2d.setColor(color != null ? color : Color.WHITE);
|
||||
g2d.setStroke(createStroke(lineType, lineWidth));
|
||||
|
||||
double x = cx - rx;
|
||||
double y = cy - ry;
|
||||
double w = rx * 2.0;
|
||||
double h = ry * 2.0;
|
||||
|
||||
if (isFull) {
|
||||
g2d.drawOval((int) Math.round(x), (int) Math.round(y), (int) Math.round(w), (int) Math.round(h));
|
||||
} else {
|
||||
Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngleDeg, sweepAngleDeg, Arc2D.OPEN);
|
||||
g2d.draw(arc);
|
||||
}
|
||||
hasContent = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a Fillet (spline / curve approximation across control points).
|
||||
*/
|
||||
public synchronized void drawFillet(int[] px, int[] py, int numPoints, Color color, int lineType, int lineWidth) {
|
||||
if (g2d == null || numPoints < 2) return;
|
||||
g2d.setColor(color != null ? color : Color.WHITE);
|
||||
g2d.setStroke(createStroke(lineType, lineWidth));
|
||||
|
||||
GeneralPath path = new GeneralPath();
|
||||
path.moveTo(px[0], py[0]);
|
||||
|
||||
if (numPoints == 2) {
|
||||
path.lineTo(px[1], py[1]);
|
||||
} else {
|
||||
for (int i = 1; i < numPoints - 1; i++) {
|
||||
double midX = (px[i] + px[i + 1]) / 2.0;
|
||||
double midY = (py[i] + py[i + 1]) / 2.0;
|
||||
path.quadTo(px[i], py[i], midX, midY);
|
||||
}
|
||||
path.lineTo(px[numPoints - 1], py[numPoints - 1]);
|
||||
}
|
||||
|
||||
g2d.draw(path);
|
||||
hasContent = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a closed polygon area with a solid color or hatching pattern.
|
||||
*/
|
||||
public synchronized void fillArea(int[] px, int[] py, int numPoints, Color fillColor, int pattern,
|
||||
boolean drawBoundary, Color boundaryColor, int lineType, int lineWidth) {
|
||||
if (g2d == null || numPoints < 3) return;
|
||||
|
||||
Polygon poly = new Polygon(px, py, numPoints);
|
||||
|
||||
if (pattern == GocaConstants.PT_SOLID || pattern == GocaConstants.PT_DEFAULT || pattern > 16) {
|
||||
g2d.setColor(fillColor != null ? fillColor : Color.WHITE);
|
||||
g2d.fill(poly);
|
||||
} else if (pattern != GocaConstants.PT_EMPTY) {
|
||||
// Fill with pattern texture
|
||||
BufferedImage patImg = createPatternTexture(pattern, fillColor != null ? fillColor : Color.WHITE);
|
||||
java.awt.TexturePaint tp = new java.awt.TexturePaint(patImg, new java.awt.Rectangle(0, 0, 8, 8));
|
||||
g2d.setPaint(tp);
|
||||
g2d.fill(poly);
|
||||
}
|
||||
|
||||
if (drawBoundary && boundaryColor != null) {
|
||||
g2d.setColor(boundaryColor);
|
||||
g2d.setStroke(createStroke(lineType, lineWidth));
|
||||
g2d.draw(poly);
|
||||
}
|
||||
hasContent = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle).
|
||||
*/
|
||||
public synchronized void drawMarker(int x, int y, int markerType, int size, Color color) {
|
||||
if (g2d == null) return;
|
||||
g2d.setColor(color != null ? color : Color.WHITE);
|
||||
g2d.setStroke(new BasicStroke(1.5f));
|
||||
|
||||
int s = Math.max(3, size > 0 ? size : 5);
|
||||
|
||||
switch (markerType) {
|
||||
case GocaConstants.MK_CROSS: // x
|
||||
g2d.drawLine(x - s, y - s, x + s, y + s);
|
||||
g2d.drawLine(x - s, y + s, x + s, y - s);
|
||||
break;
|
||||
case GocaConstants.MK_PLUS: // +
|
||||
case GocaConstants.MK_DEFAULT:
|
||||
g2d.drawLine(x - s, y, x + s, y);
|
||||
g2d.drawLine(x, y - s, x, y + s);
|
||||
break;
|
||||
case GocaConstants.MK_DIAMOND: // <>
|
||||
g2d.drawPolygon(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4);
|
||||
break;
|
||||
case GocaConstants.MK_SQUARE: // []
|
||||
g2d.drawRect(x - s, y - s, s * 2, s * 2);
|
||||
break;
|
||||
case GocaConstants.MK_6STAR: // 6-point star
|
||||
g2d.drawLine(x - s, y, x + s, y);
|
||||
g2d.drawLine(x - s / 2, y - s, x + s / 2, y + s);
|
||||
g2d.drawLine(x - s / 2, y + s, x + s / 2, y - s);
|
||||
break;
|
||||
case GocaConstants.MK_8STAR: // 8-point star
|
||||
g2d.drawLine(x - s, y, x + s, y);
|
||||
g2d.drawLine(x, y - s, x, y + s);
|
||||
g2d.drawLine(x - s, y - s, x + s, y + s);
|
||||
g2d.drawLine(x - s, y + s, x + s, y - s);
|
||||
break;
|
||||
case GocaConstants.MK_SDIAMOND: // solid diamond
|
||||
g2d.fillPolygon(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4);
|
||||
break;
|
||||
case GocaConstants.MK_SSQUARE: // solid square
|
||||
g2d.fillRect(x - s, y - s, s * 2, s * 2);
|
||||
break;
|
||||
case GocaConstants.MK_DOT: // dot
|
||||
g2d.fillOval(x - 2, y - 2, 4, 4);
|
||||
break;
|
||||
case GocaConstants.MK_CIRCLE: // circle
|
||||
g2d.drawOval(x - s, y - s, s * 2, s * 2);
|
||||
break;
|
||||
default:
|
||||
g2d.drawOval(x - s, y - s, s * 2, s * 2);
|
||||
break;
|
||||
}
|
||||
hasContent = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws stroked vector text using IBM Vector Symbol Set (VSS).
|
||||
*/
|
||||
public synchronized void drawVectorText(int x, int y, String text, Color color,
|
||||
int cellWidth, int cellHeight, int dir, double angle) {
|
||||
if (g2d == null || text == null || text.isEmpty()) return;
|
||||
g2d.setColor(color != null ? color : Color.WHITE);
|
||||
g2d.setStroke(new BasicStroke(1.2f));
|
||||
|
||||
int curX = x;
|
||||
int curY = y;
|
||||
int cw = cellWidth > 0 ? cellWidth : 12;
|
||||
int ch = cellHeight > 0 ? cellHeight : 20;
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
drawVssChar(curX, curY, c, cw, ch);
|
||||
|
||||
switch (dir) {
|
||||
case GocaConstants.CD_TB: curY += ch; break;
|
||||
case GocaConstants.CD_RL: curX -= cw; break;
|
||||
case GocaConstants.CD_BT: curY -= ch; break;
|
||||
case GocaConstants.CD_LR:
|
||||
case GocaConstants.CD_DEFAULT:
|
||||
default:
|
||||
curX += cw;
|
||||
break;
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
}
|
||||
|
||||
private void drawVssChar(int x, int y, char c, int cw, int ch) {
|
||||
int idx = (int) c;
|
||||
if (idx < VectorSymbolData.VSS_SYMBOL_START || idx > VectorSymbolData.VSS_SYMBOL_END) {
|
||||
// Draw simple bounding box or space
|
||||
return;
|
||||
}
|
||||
// Fallback vector stroke rendering
|
||||
g2d.drawString(String.valueOf(c), x, y + ch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws raw image pixel bitmap.
|
||||
*/
|
||||
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, Color fgColor) {
|
||||
if (g2d == null || imageData == null || width <= 0 || height <= 0) return;
|
||||
|
||||
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
int fgRgb = fgColor != null ? fgColor.getRGB() : 0xFFFFFFFF;
|
||||
|
||||
for (int row = 0; row < height; row++) {
|
||||
for (int col = 0; col < width; col++) {
|
||||
int bitIndex = row * width + col;
|
||||
int byteIdx = bitIndex / 8;
|
||||
if (byteIdx < imageData.length) {
|
||||
boolean bit = ((imageData[byteIdx] >> (7 - (bitIndex % 8))) & 1) != 0;
|
||||
if (bit) {
|
||||
img.setRGB(col, row, fgRgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g2d.drawImage(img, x, y, null);
|
||||
hasContent = true;
|
||||
}
|
||||
|
||||
private Stroke createStroke(int lineType, int lineWidth) {
|
||||
float width = (lineWidth == GocaConstants.LW_THICK) ? 2.5f : 1.2f;
|
||||
|
||||
switch (lineType) {
|
||||
case GocaConstants.LT_DOT:
|
||||
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f}, 0.0f);
|
||||
case GocaConstants.LT_SHORTDASH:
|
||||
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{4.0f, 2.0f}, 0.0f);
|
||||
case GocaConstants.LT_DASHDOT:
|
||||
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{6.0f, 2.0f, 2.0f, 2.0f}, 0.0f);
|
||||
case GocaConstants.LT_DOUBLEDOT:
|
||||
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f, 2.0f, 4.0f}, 0.0f);
|
||||
case GocaConstants.LT_LONGDASH:
|
||||
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{8.0f, 3.0f}, 0.0f);
|
||||
case GocaConstants.LT_DASHDOUBLEDOT:
|
||||
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{8.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f}, 0.0f);
|
||||
case GocaConstants.LT_SOLID:
|
||||
case GocaConstants.LT_DEFAULT:
|
||||
default:
|
||||
return new BasicStroke(width);
|
||||
}
|
||||
}
|
||||
|
||||
private BufferedImage createPatternTexture(int patternIndex, Color color) {
|
||||
BufferedImage pat = new BufferedImage(8, 8, BufferedImage.TYPE_INT_ARGB);
|
||||
byte[] patternRows = (patternIndex >= 0 && patternIndex < PATTERN_DATA.length) ? PATTERN_DATA[patternIndex] : PATTERN_DATA[0];
|
||||
int rgb = color.getRGB();
|
||||
|
||||
for (int r = 0; r < 8; r++) {
|
||||
int b = patternRows[r] & 0xFF;
|
||||
for (int c = 0; c < 8; c++) {
|
||||
if (((b >> (7 - c)) & 1) != 0) {
|
||||
pat.setRGB(c, r, rgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
return pat;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package org.lib3270j.graphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.util.Arrays;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Manages IBM 3270 Programmed Symbols (PS / APL) character sets.
|
||||
* Handles the Load Programmed Symbols (LOADPS structured field 0x0F).
|
||||
*/
|
||||
public class ProgramSymbolManager {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(ProgramSymbolManager.class.getName());
|
||||
|
||||
public static final int NUMBER_SYMBOL_SETS = 10;
|
||||
public static final int NUMBER_SINGLE_PLANE_PS_SETS = 2; // RWS 2..3 (Sets 0..1)
|
||||
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 final ProgramSymbolSet[] sets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all symbol sets.
|
||||
*/
|
||||
public synchronized void clearAll() {
|
||||
for (ProgramSymbolSet set : sets) {
|
||||
set.clear();
|
||||
set.setLcid(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the symbol set corresponding to a given LCID (0x40 - 0xFE).
|
||||
*/
|
||||
public synchronized ProgramSymbolSet getSymbolSet(int lcid) {
|
||||
if (lcid <= 0) {
|
||||
return null;
|
||||
}
|
||||
for (ProgramSymbolSet set : sets) {
|
||||
if (set.getLcid() == lcid) {
|
||||
return set;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
if (set == null) {
|
||||
return null;
|
||||
}
|
||||
int index = (codePoint >= 0x40) ? (codePoint - 0x40) : codePoint;
|
||||
return set.getSlot(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a programmed symbol character cell if defined.
|
||||
* Returns true if symbol was drawn, false if not found.
|
||||
*/
|
||||
public synchronized boolean drawSymbol(Graphics2D g2d, int lcid, int codePoint,
|
||||
int x, int y, int cellWidth, int cellHeight,
|
||||
Color fgColor, Color bgColor) {
|
||||
ProgramSymbolSet.SymbolSlot slot = getSymbol(lcid, codePoint);
|
||||
if (slot != null) {
|
||||
slot.draw(g2d, x, y, cellWidth, cellHeight, fgColor, bgColor);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a Load Programmed Symbols (LOADPS structured field 0x0F) payload.
|
||||
*/
|
||||
public synchronized void loadps(byte[] data) {
|
||||
if (data == null || data.length < 4) {
|
||||
logger.warning("LOADPS: Payload too short (" + (data == null ? 0 : data.length) + " bytes)");
|
||||
return;
|
||||
}
|
||||
|
||||
int flags = data[0] & 0xFF;
|
||||
int loadFormat = flags & 0x1F;
|
||||
boolean clearAll = (flags & 0x40) != 0;
|
||||
boolean hasExtHeader = (flags & 0x80) != 0;
|
||||
|
||||
int lcid = data[1] & 0xFF;
|
||||
int startCodePoint = data[2] & 0xFF;
|
||||
int rws = data[3] & 0xFF;
|
||||
|
||||
int setIndex;
|
||||
switch (rws) {
|
||||
case 2: setIndex = 0; break;
|
||||
case 3: setIndex = 1; break;
|
||||
case 4: setIndex = 2; break;
|
||||
case 5: setIndex = 3; break;
|
||||
case 6: setIndex = 4; break;
|
||||
case 7: setIndex = 5; break;
|
||||
case 8: setIndex = 6; break;
|
||||
case 9: setIndex = 7; break;
|
||||
case 10: setIndex = 8; break;
|
||||
case 11: setIndex = 9; break;
|
||||
default:
|
||||
logger.warning("LOADPS: Invalid RWS slot 0x" + Integer.toHexString(rws));
|
||||
return;
|
||||
}
|
||||
|
||||
ProgramSymbolSet set = sets[setIndex];
|
||||
boolean isTriplePlane = (rws >= 4 && rws <= 7);
|
||||
|
||||
int extHeaderLen = 0;
|
||||
int cellWidth = 9;
|
||||
int cellHeight = 16;
|
||||
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
|
||||
|
||||
if (hasExtHeader && data.length > 4) {
|
||||
extHeaderLen = data[4] & 0xFF;
|
||||
if (extHeaderLen > 3 && data.length > 6) {
|
||||
int lw = data[6] & 0xFF;
|
||||
if (lw > 0) cellWidth = lw;
|
||||
}
|
||||
if (extHeaderLen > 4 && data.length > 7) {
|
||||
int lh = data[7] & 0xFF;
|
||||
if (lh > 0) cellHeight = lh;
|
||||
}
|
||||
if (extHeaderLen >= 6 && data.length > 9) {
|
||||
colorPlane = data[9] & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
if (clearAll) {
|
||||
set.clear();
|
||||
}
|
||||
set.setLcid(lcid);
|
||||
|
||||
int offset = 4 + (hasExtHeader ? extHeaderLen : 0);
|
||||
int remaining = data.length - offset;
|
||||
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
|
||||
|
||||
int bytesPerSymbol;
|
||||
if (loadFormat == 1) {
|
||||
bytesPerSymbol = 18; // 9x16 cell in standard transmission format
|
||||
} else {
|
||||
bytesPerSymbol = (cellWidth * cellHeight + 7) / 8;
|
||||
}
|
||||
|
||||
if (bytesPerSymbol <= 0) {
|
||||
bytesPerSymbol = 18;
|
||||
}
|
||||
|
||||
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
|
||||
byte[] pixelData;
|
||||
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 (loadFormat == 1) {
|
||||
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
} else {
|
||||
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
}
|
||||
|
||||
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
|
||||
|
||||
codeIndex++;
|
||||
offset += bytesPerSymbol;
|
||||
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)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks Format 1 (9x16) symbol slice bit-pattern.
|
||||
*/
|
||||
private void unpackFormat1(byte[] src, int srcOff, byte[] dst, int width, int height,
|
||||
boolean isTriplePlane, int colorPlane) {
|
||||
// Format 1 transmits 18 bytes:
|
||||
// Byte 0-1: contains column 0 for each of the 16 rows
|
||||
// Bytes 2-17: contains columns 1-8 for each of the 16 rows
|
||||
int planeMask = (colorPlane != 0) ? colorPlane : (isTriplePlane ? 7 : 1);
|
||||
|
||||
for (int row = 0; row < 16 && row < height; row++) {
|
||||
// Column 0 bit from byte 0 or byte 1
|
||||
int b0 = (row < 8) ? (src[srcOff] & 0xFF) : (src[srcOff + 1] & 0xFF);
|
||||
int bitShift0 = 7 - (row % 8);
|
||||
boolean bit0 = ((b0 >> bitShift0) & 1) != 0;
|
||||
|
||||
int dstIdx0 = row * width;
|
||||
if (dstIdx0 < dst.length) {
|
||||
if (!isTriplePlane) {
|
||||
dst[dstIdx0] = bit0 ? (byte) 1 : 0;
|
||||
} else if (colorPlane == 0) {
|
||||
dst[dstIdx0] = bit0 ? (byte) 7 : 0;
|
||||
} else {
|
||||
if (bit0) {
|
||||
dst[dstIdx0] |= (byte) colorPlane;
|
||||
} else {
|
||||
dst[dstIdx0] &= (byte) ~colorPlane;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Columns 1..8 from bytes 2..17
|
||||
if (srcOff + 2 + row < src.length) {
|
||||
int rowByte = src[srcOff + 2 + row] & 0xFF;
|
||||
for (int col = 1; col < 9 && col < width; col++) {
|
||||
int dstIdx = row * width + col;
|
||||
if (dstIdx < dst.length) {
|
||||
boolean bit = ((rowByte >> (8 - col)) & 1) != 0;
|
||||
if (!isTriplePlane) {
|
||||
dst[dstIdx] = bit ? (byte) 1 : 0;
|
||||
} else if (colorPlane == 0) {
|
||||
dst[dstIdx] = bit ? (byte) 7 : 0;
|
||||
} else {
|
||||
if (bit) {
|
||||
dst[dstIdx] |= (byte) colorPlane;
|
||||
} else {
|
||||
dst[dstIdx] &= (byte) ~colorPlane;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks Format 3 variable dimension bit-pattern.
|
||||
*/
|
||||
private void unpackFormat3(byte[] src, int srcOff, byte[] dst, int width, int height,
|
||||
boolean isTriplePlane, int colorPlane) {
|
||||
int totalPixels = width * height;
|
||||
for (int i = 0; i < totalPixels; i++) {
|
||||
int byteIndex = srcOff + (i / 8);
|
||||
if (byteIndex >= src.length) break;
|
||||
int bitIndex = 7 - (i % 8);
|
||||
boolean bit = ((src[byteIndex] >> bitIndex) & 1) != 0;
|
||||
|
||||
if (!isTriplePlane) {
|
||||
dst[i] = bit ? (byte) 1 : 0;
|
||||
} else if (colorPlane == 0) {
|
||||
dst[i] = bit ? (byte) 7 : 0;
|
||||
} else {
|
||||
if (bit) {
|
||||
dst[i] |= (byte) colorPlane;
|
||||
} else {
|
||||
dst[i] &= (byte) ~colorPlane;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package org.lib3270j.graphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Image;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE).
|
||||
* Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite).
|
||||
*/
|
||||
public class ProgramSymbolSet {
|
||||
|
||||
public static final int NUM_SLOTS = 191; // Code points 0x40 - 0xFE (0..190)
|
||||
|
||||
private int lcid = 0;
|
||||
private final boolean isTriplePlane;
|
||||
private final SymbolSlot[] slots = new SymbolSlot[NUM_SLOTS];
|
||||
|
||||
public ProgramSymbolSet(boolean isTriplePlane) {
|
||||
this.isTriplePlane = isTriplePlane;
|
||||
}
|
||||
|
||||
public int getLcid() {
|
||||
return lcid;
|
||||
}
|
||||
|
||||
public void setLcid(int lcid) {
|
||||
this.lcid = lcid;
|
||||
}
|
||||
|
||||
public boolean isTriplePlane() {
|
||||
return isTriplePlane;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
for (int i = 0; i < NUM_SLOTS; i++) {
|
||||
slots[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void clearSlot(int index) {
|
||||
if (index >= 0 && index < NUM_SLOTS) {
|
||||
slots[index] = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSlot(int index, SymbolSlot slot) {
|
||||
if (index >= 0 && index < NUM_SLOTS) {
|
||||
slots[index] = slot;
|
||||
}
|
||||
}
|
||||
|
||||
public SymbolSlot getSlot(int index) {
|
||||
if (index >= 0 && index < NUM_SLOTS) {
|
||||
return slots[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single custom symbol bitmap.
|
||||
*/
|
||||
public static class SymbolSlot {
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome)
|
||||
private final boolean isTriplePlane;
|
||||
private BufferedImage cachedImage;
|
||||
private int cachedFgRgb = -1;
|
||||
private int cachedBgRgb = -1;
|
||||
|
||||
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) {
|
||||
this.width = width > 0 ? width : 9;
|
||||
this.height = height > 0 ? height : 16;
|
||||
this.pixelData = pixelData;
|
||||
this.isTriplePlane = isTriplePlane;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public byte[] getPixelData() {
|
||||
return pixelData;
|
||||
}
|
||||
|
||||
public boolean isTriplePlane() {
|
||||
return isTriplePlane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders this symbol to a BufferedImage.
|
||||
*/
|
||||
public synchronized BufferedImage getImage(Color fgColor, Color bgColor) {
|
||||
int fgRgb = fgColor != null ? fgColor.getRGB() : 0xFFFFFFFF;
|
||||
int bgRgb = bgColor != null ? bgColor.getRGB() : 0x00000000;
|
||||
|
||||
if (cachedImage != null && cachedFgRgb == fgRgb && cachedBgRgb == bgRgb) {
|
||||
return cachedImage;
|
||||
}
|
||||
|
||||
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
int[] rgbArray = new int[width * height];
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int idx = y * width + x;
|
||||
int val = (idx < pixelData.length) ? (pixelData[idx] & 0xFF) : 0;
|
||||
|
||||
if (val == 0) {
|
||||
rgbArray[idx] = bgRgb;
|
||||
} else if (!isTriplePlane) {
|
||||
rgbArray[idx] = fgRgb;
|
||||
} else {
|
||||
// Triple-Plane RGB composite:
|
||||
// val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
|
||||
int r = (val & 0x01) != 0 ? 255 : 0;
|
||||
int g = (val & 0x02) != 0 ? 255 : 0;
|
||||
int b = (val & 0x04) != 0 ? 255 : 0;
|
||||
rgbArray[idx] = (0xFF << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img.setRGB(0, 0, width, height, rgbArray, 0, width);
|
||||
this.cachedImage = img;
|
||||
this.cachedFgRgb = fgRgb;
|
||||
this.cachedBgRgb = bgRgb;
|
||||
return img;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the symbol directly onto a Graphics2D surface with aspect scaling.
|
||||
*/
|
||||
public void draw(Graphics2D g2d, int x, int y, int cellWidth, int cellHeight, Color fgColor, Color bgColor) {
|
||||
BufferedImage img = getImage(fgColor, bgColor);
|
||||
if (img != null) {
|
||||
g2d.drawImage(img, x, y, cellWidth, cellHeight, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -31,6 +31,12 @@ public class InputProcessor {
|
||||
this.fsm = fsm;
|
||||
}
|
||||
|
||||
private org.lib3270j.graphics.GraphicsPlane graphicsPlane;
|
||||
|
||||
public void setGraphicsPlane(org.lib3270j.graphics.GraphicsPlane gp) {
|
||||
this.graphicsPlane = gp;
|
||||
}
|
||||
|
||||
public enum OiaStatus {
|
||||
NOT_CONNECTED("OFFLINE"),
|
||||
X_SYSTEM("X SYSTEM"),
|
||||
@@ -160,6 +166,9 @@ public class InputProcessor {
|
||||
if (aidCode == AID_CLEAR) {
|
||||
screen.clear();
|
||||
screen.markAllChanged();
|
||||
if (graphicsPlane != null) {
|
||||
graphicsPlane.clear();
|
||||
}
|
||||
// Send just the AID
|
||||
byte[] data = new byte[] { (byte) aidCode };
|
||||
sendAidResponse(data);
|
||||
|
||||
@@ -202,16 +202,25 @@ public final class DS3270Constants {
|
||||
public static final int QR_SUMMARY = 0x80;
|
||||
public static final int QR_USABLE_AREA = 0x81;
|
||||
public static final int QR_IMAGE = 0x82;
|
||||
public static final int QR_TEXT_PART = 0x83;
|
||||
public static final int QR_TEXT_PART = 0x83;
|
||||
public static final int QR_ALPHA_PART = 0x84;
|
||||
public static final int QR_CHARSETS = 0x85;
|
||||
public static final int QR_COLOR = 0x86;
|
||||
public static final int QR_HIGHLIGHTING = 0x87;
|
||||
public static final int QR_REPLY_MODES = 0x88;
|
||||
public static final int QR_SAVE_RESTORE = 0x8c;
|
||||
public static final int QR_DBCS_ASIA = 0x91;
|
||||
public static final int QR_DDM = 0x95;
|
||||
public static final int QR_TRANSPARENCY = 0x99;
|
||||
public static final int QR_RPQNAMES = 0xa1;
|
||||
public static final int QR_IMP_PART = 0xa6;
|
||||
public static final int QR_RPQ_NAMES = 0xa8;
|
||||
public static final int QR_GRAPHICS = 0xb0; // 3270-PC Vector Graphics
|
||||
public static final int QR_GIMAGE = 0xb1; // Image / Graphics Planes
|
||||
public static final int QR_AUX_DEV = 0xb2; // Auxiliary Device
|
||||
public static final int QR_OEM_FMT = 0xb3; // OEM Format
|
||||
public static final int QR_GCOLOR = 0xb4; // Graphic Color Table
|
||||
public static final int QR_GSYMBOLS = 0xb6; // Graphic Symbol Sets
|
||||
public static final int QR_NULL = 0xff;
|
||||
|
||||
// ========== Screen model sizes ==========
|
||||
|
||||
@@ -28,26 +28,42 @@ public class TelnetConnection {
|
||||
private final TelnetFSM fsm;
|
||||
private final ConnectionConfig config;
|
||||
|
||||
private javax.net.ssl.SSLSession sslSession;
|
||||
|
||||
public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) {
|
||||
this.config = config;
|
||||
this.fsm = fsm;
|
||||
}
|
||||
|
||||
public javax.net.ssl.SSLSession getSslSession() {
|
||||
return sslSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the host. Blocks until connection is established or fails.
|
||||
*/
|
||||
public void connect() throws IOException {
|
||||
if (config.isUseTls()) {
|
||||
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort());
|
||||
javax.net.ssl.SSLSocketFactory ssf = (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault();
|
||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
||||
sslSocket.setKeepAlive(true);
|
||||
sslSocket.setOOBInline(true);
|
||||
sslSocket.setTcpNoDelay(true);
|
||||
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||
config.getConnectTimeoutMs());
|
||||
sslSocket.startHandshake();
|
||||
socket = sslSocket;
|
||||
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort() +
|
||||
" (verifyCert=" + config.isTlsVerifyCert() + ")");
|
||||
try {
|
||||
javax.net.ssl.SSLContext sslContext = org.lib3270j.tls.TlsTrustManager.createSSLContext(config);
|
||||
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
|
||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
||||
sslSocket.setKeepAlive(true);
|
||||
sslSocket.setTcpNoDelay(true);
|
||||
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||
config.getConnectTimeoutMs());
|
||||
sslSocket.startHandshake();
|
||||
socket = sslSocket;
|
||||
sslSession = sslSocket.getSession();
|
||||
log.info("TLS session active: protocol=" + sslSession.getProtocol() +
|
||||
" cipher=" + sslSession.getCipherSuite());
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IOException("TLS setup failure: " + e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
|
||||
socket = new Socket();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.lib3270j.tls;
|
||||
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* Callback interface for validating TLS server certificates.
|
||||
* Used when standard certificate path validation fails or when custom verification is needed.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TlsCertificateVerifier {
|
||||
|
||||
/**
|
||||
* Determine whether to trust an unverified server certificate chain.
|
||||
*
|
||||
* @param chain The peer certificate chain presented by the server.
|
||||
* @param authType The key exchange algorithm (e.g., "RSA", "ECDHE_RSA").
|
||||
* @param exception The CertificateException thrown by standard validation (or null if called proactively).
|
||||
* @return true to trust the certificate and proceed with the connection; false to abort.
|
||||
*/
|
||||
boolean shouldTrust(X509Certificate[] chain, String authType, CertificateException exception);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package org.lib3270j.tls;
|
||||
|
||||
import org.lib3270j.ConnectionConfig;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Custom X509TrustManager that supports:
|
||||
* 1. Standard certificate verification using the JVM default TrustManager.
|
||||
* 2. Unverified / trust-all mode when tlsVerifyCert is false.
|
||||
* 3. Interactive/custom certificate verifier callbacks (e.g. GUI prompts for self-signed certificates).
|
||||
*/
|
||||
public class TlsTrustManager implements X509TrustManager {
|
||||
|
||||
private static final Logger log = Logger.getLogger(TlsTrustManager.class.getName());
|
||||
|
||||
private final ConnectionConfig config;
|
||||
private X509TrustManager defaultTrustManager;
|
||||
|
||||
public TlsTrustManager(ConnectionConfig config) {
|
||||
this.config = config;
|
||||
try {
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init((KeyStore) null);
|
||||
for (TrustManager tm : tmf.getTrustManagers()) {
|
||||
if (tm instanceof X509TrustManager) {
|
||||
this.defaultTrustManager = (X509TrustManager) tm;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Failed to initialize default TrustManagerFactory", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
if (defaultTrustManager != null) {
|
||||
defaultTrustManager.checkClientTrusted(chain, authType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
if (config != null && !config.isTlsVerifyCert()) {
|
||||
log.fine("Certificate verification bypassed (tlsVerifyCert=false)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (chain == null || chain.length == 0) {
|
||||
CertificateException ex = new CertificateException("null or zero-length certificate chain");
|
||||
if (config != null && config.getCertificateVerifier() != null) {
|
||||
if (config.getCertificateVerifier().shouldTrust(chain, authType, ex)) {
|
||||
log.info("Server certificate accepted via TlsCertificateVerifier callback");
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
|
||||
try {
|
||||
if (defaultTrustManager != null) {
|
||||
defaultTrustManager.checkServerTrusted(chain, authType);
|
||||
} else {
|
||||
throw new CertificateException("No default X509TrustManager available");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
CertificateException certEx = (ex instanceof CertificateException)
|
||||
? (CertificateException) ex
|
||||
: new CertificateException("Certificate validation failed: " + ex.getMessage(), ex);
|
||||
|
||||
log.log(Level.FINE, "Standard certificate validation failed: " + certEx.getMessage(), certEx);
|
||||
|
||||
if (config != null && config.getCertificateVerifier() != null) {
|
||||
boolean accepted = config.getCertificateVerifier().shouldTrust(chain, authType, certEx);
|
||||
if (accepted) {
|
||||
log.info("Server certificate accepted via TlsCertificateVerifier callback");
|
||||
return;
|
||||
} else {
|
||||
log.warning("Server certificate rejected by TlsCertificateVerifier callback");
|
||||
throw new CertificateException("Certificate rejected by user/verifier: " + certEx.getMessage(), certEx);
|
||||
}
|
||||
}
|
||||
throw certEx;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
if (defaultTrustManager != null) {
|
||||
return defaultTrustManager.getAcceptedIssuers();
|
||||
}
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an initialized SSLContext configured for the given ConnectionConfig.
|
||||
*/
|
||||
public static SSLContext createSSLContext(ConnectionConfig config) throws Exception {
|
||||
String protocol = (config != null && config.getSslProtocol() != null)
|
||||
? config.getSslProtocol()
|
||||
: "TLS";
|
||||
SSLContext sslContext = SSLContext.getInstance(protocol);
|
||||
TlsTrustManager trustManager = new TlsTrustManager(config);
|
||||
sslContext.init(null, new TrustManager[] { trustManager }, new SecureRandom());
|
||||
return sslContext;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.lib3270j.datastream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lib3270j.TerminalModel;
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
import org.lib3270j.graphics.GraphicsMode;
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
@@ -14,11 +15,43 @@ public class QueryReplyBuilderTest {
|
||||
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
|
||||
|
||||
@Test
|
||||
public void testBuildAllQueryReplies() {
|
||||
public void testBuildAllQueryRepliesDefaultNone() {
|
||||
assertEquals(GraphicsMode.NONE, qrBuilder.getGraphicsMode());
|
||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||
assertNotNull(replies);
|
||||
assertTrue(replies.length > 0);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
|
||||
// In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT 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;
|
||||
}
|
||||
}
|
||||
assertFalse(hasB0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAllQueryRepliesWithVectorGraphics() {
|
||||
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
|
||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||
assertNotNull(replies);
|
||||
|
||||
// Vector Graphics QR 0xB0 and 0xB4 must be present
|
||||
boolean hasB0 = false;
|
||||
boolean hasB4 = false;
|
||||
for (int i = 0; i < replies.length - 3; i++) {
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
||||
hasB0 = true;
|
||||
}
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GCOLOR) {
|
||||
hasB4 = true;
|
||||
}
|
||||
}
|
||||
assertTrue(hasB0);
|
||||
assertTrue(hasB4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -52,4 +85,29 @@ public class QueryReplyBuilderTest {
|
||||
assertEquals(0x81, replies[3] & 0xFF);
|
||||
assertEquals(QR_NULL, replies[4] & 0xFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImplicitPartitionModel4() {
|
||||
byte[] requested = new byte[] { (byte) QR_IMP_PART };
|
||||
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||
|
||||
assertNotNull(replies);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
int len = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
|
||||
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
|
||||
assertEquals(QR_IMP_PART, replies[4] & 0xFF); // 0xA6
|
||||
|
||||
// Payload starts at index 5: 22 bytes total
|
||||
// Default size = 80 x 24
|
||||
int defCols = ((replies[10] & 0xFF) << 8) | (replies[11] & 0xFF);
|
||||
int defRows = ((replies[12] & 0xFF) << 8) | (replies[13] & 0xFF);
|
||||
assertEquals(80, defCols);
|
||||
assertEquals(24, defRows);
|
||||
|
||||
// Alt size = 80 x 43
|
||||
int altCols = ((replies[14] & 0xFF) << 8) | (replies[15] & 0xFF);
|
||||
int altRows = ((replies[16] & 0xFF) << 8) | (replies[17] & 0xFF);
|
||||
assertEquals(80, altCols);
|
||||
assertEquals(43, altRows);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
package org.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class GocaDecoderTest {
|
||||
|
||||
@Test
|
||||
public void testBasicLineAndColorOrders() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
assertFalse(plane.hasContent());
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Set Color to Red (GOCA Color 2) - 2 bytes: order, color
|
||||
out.write(GocaConstants.G_GSCOL);
|
||||
out.write(0x02); // Red
|
||||
|
||||
// Line from (1000, 1000) to (2000, 2000) - long order: order, len, x1, y1, x2, y2
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x08); // 8 bytes: (x1, y1, x2, y2)
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAreaAndPatternFill() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Set Pattern to Solid - 2 bytes: order, pattern
|
||||
out.write(GocaConstants.G_GSPT);
|
||||
out.write(GocaConstants.PT_SOLID);
|
||||
|
||||
// Begin Area - 2 bytes: order, flags
|
||||
out.write(GocaConstants.G_GBAR);
|
||||
out.write(0x00);
|
||||
|
||||
// Polyline forming a triangle: (500, 500) -> (1500, 500) -> (1000, 1500) -> (500, 500)
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x10); // 4 points * 4 bytes = 16 bytes
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
|
||||
out.write((1500 >> 8) & 0xFF); out.write(1500 & 0xFF);
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write((1500 >> 8) & 0xFF); out.write(1500 & 0xFF);
|
||||
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
|
||||
// End Area - 1 byte
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMarkerAndVectorText() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Set Marker Type to Circle - 2 bytes: order, type
|
||||
out.write(GocaConstants.G_GSMT);
|
||||
out.write(GocaConstants.MK_CIRCLE);
|
||||
|
||||
// Draw Marker at (2000, 2000)
|
||||
out.write(GocaConstants.G_GMRK);
|
||||
out.write(0x04);
|
||||
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||
|
||||
// Draw Vector Stroked Character String: "IBM" (EBCDIC: 0xC9, 0xC2, 0xD4)
|
||||
out.write(GocaConstants.G_GCHST);
|
||||
out.write(0x07); // 4 bytes pos + 3 bytes chars
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write(0xC9); out.write(0xC2); out.write(0xD4);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcedureOrdersAndErase() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Draw something first
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x08);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x64);
|
||||
out.write(0x00); out.write(0x64);
|
||||
byte[] drawStream = out.toByteArray();
|
||||
decoder.decodeStream(drawStream, 0, drawStream.length);
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
// Process procedure order 0x0A (Erase presentation space)
|
||||
byte[] procOrders = new byte[] { (byte) GocaConstants.P_ERASE };
|
||||
decoder.processProcedureOrders(procOrders, 0, procOrders.length);
|
||||
assertFalse(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSegmentAndRelativeLineOrders() {
|
||||
GraphicsPlane plane = new GraphicsPlane(720, 688);
|
||||
plane.setScreenDimensions(80, 43);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// 0x70: Begin Segment (14 bytes total)
|
||||
out.write(GocaConstants.G_BEGSEGM);
|
||||
out.write(0x0C);
|
||||
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x01); // Seg ID 1
|
||||
out.write(0x74); out.write(0x70);
|
||||
out.write(0x00); out.write(0x1E); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
|
||||
// 0x3E: End Prologue
|
||||
out.write(GocaConstants.G_ENDPROLOGUE);
|
||||
out.write(0x00);
|
||||
|
||||
// 0x0A: Color (7 = White)
|
||||
out.write(GocaConstants.G_GSCOL);
|
||||
out.write(0x07);
|
||||
|
||||
// 0xE1: Relative Line (Absolute Start (300, 200), deltas: (+10, -5), (-10, +5))
|
||||
out.write(GocaConstants.G_GRLINE);
|
||||
out.write(0x08); // 4 bytes start coord + 4 bytes (2 deltas) = 8 bytes
|
||||
out.write(0x01); out.write(0x2C); // Start X = 300
|
||||
out.write(0x00); out.write(0xC8); // Start Y = 200
|
||||
out.write(0x0A); out.write(0xFB); // dx = +10, dy = -5
|
||||
out.write(0xF6); out.write(0x05); // dx = -10, dy = +5
|
||||
|
||||
// 0x71: End Segment
|
||||
out.write(GocaConstants.G_ENDSEGM);
|
||||
out.write(0x00);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartialOrderBufferingAcrossStreams() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// First packet sends G_GLINE order code and half of its coordinates
|
||||
ByteArrayOutputStream chunk1 = new ByteArrayOutputStream();
|
||||
chunk1.write(GocaConstants.G_GLINE);
|
||||
chunk1.write(0x08); // 8 bytes: 2 points
|
||||
chunk1.write(0x00); chunk1.write(0x64); // x1 = 100
|
||||
chunk1.write(0x00); chunk1.write(0x64); // y1 = 100
|
||||
|
||||
byte[] b1 = chunk1.toByteArray();
|
||||
decoder.decodeStream(b1, 0, b1.length);
|
||||
// Order is not complete yet, so no line drawn yet
|
||||
assertFalse(plane.hasContent());
|
||||
|
||||
// Second packet sends the rest of the order: x2 = 200, y2 = 200
|
||||
ByteArrayOutputStream chunk2 = new ByteArrayOutputStream();
|
||||
chunk2.write(0x00); chunk2.write(0xC8); // x2 = 200
|
||||
chunk2.write(0x00); chunk2.write(0xC8); // y2 = 200
|
||||
|
||||
byte[] b2 = chunk2.toByteArray();
|
||||
decoder.decodeStream(b2, 0, b2.length);
|
||||
// Now the combined order executes!
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetCurrentPositionAndRelativeLine() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// 0x21: Set Current Position to (50, 50)
|
||||
out.write(GocaConstants.G_GSCP);
|
||||
out.write(0x04); // len = 4
|
||||
out.write(0x00); out.write(0x32); // x = 50
|
||||
out.write(0x00); out.write(0x32); // y = 50
|
||||
|
||||
// 0xA1: Relative Line from Current Position: (+10, +20), (-5, -10)
|
||||
out.write(GocaConstants.G_GCRLIN);
|
||||
out.write(0x04); // len = 4 (2 steps)
|
||||
out.write(0x0A); out.write(0x14); // dx = +10, dy = +20
|
||||
out.write(0xFB); out.write(0xF6); // dx = -5, dy = -10
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
assertEquals(55, decoder.getCurX());
|
||||
assertEquals(60, decoder.getCurY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test3179GCoordinateMapping() {
|
||||
GraphicsPlane plane = new GraphicsPlane(720, 688);
|
||||
plane.setScreenDimensions(80, 43);
|
||||
|
||||
// Screen center (0, 0) should map to canvas center (360, 343)
|
||||
assertEquals(360, plane.mapX(0));
|
||||
assertEquals(343, plane.mapY(0));
|
||||
|
||||
// Left edge (-360) should map to 0
|
||||
assertEquals(0, plane.mapX(-360));
|
||||
// Right edge (+359) should map to 719
|
||||
assertEquals(719, plane.mapX(359));
|
||||
|
||||
// Top edge (+343) should map to 0
|
||||
assertEquals(0, plane.mapY(343));
|
||||
// Bottom edge (-344) should map to 687
|
||||
assertEquals(687, plane.mapY(-344));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lib3270j.ConnectionConfig;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class GraphicsModeTest {
|
||||
|
||||
@Test
|
||||
public void testDefaultsAndParsing() {
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString(null));
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString(""));
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("NONE"));
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("off"));
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("false"));
|
||||
|
||||
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("ps"));
|
||||
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("programmed_symbols"));
|
||||
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("apl"));
|
||||
|
||||
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("vector"));
|
||||
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("goca"));
|
||||
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("vector_graphics"));
|
||||
|
||||
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("both"));
|
||||
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("all"));
|
||||
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("true"));
|
||||
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("on"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFlags() {
|
||||
assertFalse(GraphicsMode.NONE.isProgrammedSymbolsEnabled());
|
||||
assertFalse(GraphicsMode.NONE.isVectorGraphicsEnabled());
|
||||
|
||||
assertTrue(GraphicsMode.PROGRAMMED_SYMBOLS.isProgrammedSymbolsEnabled());
|
||||
assertFalse(GraphicsMode.PROGRAMMED_SYMBOLS.isVectorGraphicsEnabled());
|
||||
|
||||
assertFalse(GraphicsMode.VECTOR_GRAPHICS.isProgrammedSymbolsEnabled());
|
||||
assertTrue(GraphicsMode.VECTOR_GRAPHICS.isVectorGraphicsEnabled());
|
||||
|
||||
assertTrue(GraphicsMode.BOTH.isProgrammedSymbolsEnabled());
|
||||
assertTrue(GraphicsMode.BOTH.isVectorGraphicsEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectionConfigDefault() {
|
||||
ConnectionConfig config = new ConnectionConfig();
|
||||
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
||||
|
||||
config.setGraphicsMode(GraphicsMode.BOTH);
|
||||
assertEquals(GraphicsMode.BOTH, config.getGraphicsMode());
|
||||
|
||||
config.setGraphicsMode(null);
|
||||
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package org.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ProgramSymbolManagerTest {
|
||||
|
||||
@Test
|
||||
public void testSetAllocationAndLookup() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
// Loadable set can be queried after loadps or clearAll
|
||||
assertNull(manager.getSymbolSet(0x40));
|
||||
assertNull(manager.getSymbol(0x40, 0x41));
|
||||
|
||||
manager.clearAll();
|
||||
assertNull(manager.getSymbolSet(0x40));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSinglePlaneLoadPsFormat1() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
|
||||
// Build Format 1 Load PS payload for LCID 0x40, code point 0x41 ('A')
|
||||
// Format 1 header:
|
||||
// byte 0: flags (0x01: format 1, single plane)
|
||||
// byte 1: LCID (0x40)
|
||||
// byte 2: Start code point (0x41)
|
||||
// byte 3: RWS (0x02: loadable slot 2)
|
||||
// followed by 18 bytes per symbol for 9x16 cell:
|
||||
// Byte 0-1: Col 0 for rows 0..15 (0x80 = bit for row 0 col 0)
|
||||
// Bytes 2-17: Cols 1..8 for rows 0..15 (0xFF = bits for row 0 cols 1..8)
|
||||
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; // Row 0 Col 0 bit
|
||||
payload[5] = (byte) 0x00; // Rows 8..15 Col 0
|
||||
for (int r = 0; r < 16; r++) {
|
||||
payload[6 + r] = (byte) 0xFF; // Cols 1..8 on all rows
|
||||
}
|
||||
|
||||
manager.loadps(payload);
|
||||
|
||||
ProgramSymbolSet set = manager.getSymbolSet(0x40);
|
||||
assertNotNull(set);
|
||||
assertEquals(0x40, set.getLcid());
|
||||
assertFalse(set.isTriplePlane());
|
||||
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
|
||||
assertNotNull(slot);
|
||||
assertEquals(9, slot.getWidth());
|
||||
assertEquals(16, slot.getHeight());
|
||||
assertFalse(slot.isTriplePlane());
|
||||
|
||||
// Verify pixel data (Row 0 Col 0 is 1)
|
||||
byte[] pixels = slot.getPixelData();
|
||||
assertNotNull(pixels);
|
||||
assertEquals(1, pixels[0]); // Row 0 Col 0 is 1
|
||||
|
||||
// Test rendering into BufferedImage
|
||||
BufferedImage canvas = new BufferedImage(80, 24, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2 = canvas.createGraphics();
|
||||
boolean drawn = manager.drawSymbol(g2, 0x40, 0x41, 0, 0, 10, 20, Color.GREEN, Color.BLACK);
|
||||
g2.dispose();
|
||||
assertTrue(drawn);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTriplePlaneMultiColorComposite() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
|
||||
// Build Format 1 Load PS payload for LCID 0x42, triple plane
|
||||
// flags = 0x01 (Format 1)
|
||||
// RWS = 4 (Triple Plane)
|
||||
byte[] payload = new byte[4 + 18];
|
||||
payload[0] = 0x01; // Format 1
|
||||
payload[1] = 0x42; // LCID 0x42
|
||||
payload[2] = 0x45; // Code point 0x45
|
||||
payload[3] = 0x04; // RWS 4 (Triple Plane)
|
||||
|
||||
// Set pixel at row 0 col 0
|
||||
payload[4] = (byte) 0x80; // Row 0 Col 0
|
||||
payload[5] = (byte) 0x00;
|
||||
for (int r = 0; r < 16; r++) {
|
||||
payload[6 + r] = (byte) 0x00;
|
||||
}
|
||||
|
||||
manager.loadps(payload);
|
||||
|
||||
ProgramSymbolSet set = manager.getSymbolSet(0x42);
|
||||
assertNotNull(set);
|
||||
assertTrue(set.isTriplePlane());
|
||||
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x42, 0x45);
|
||||
assertNotNull(slot);
|
||||
assertTrue(slot.isTriplePlane());
|
||||
|
||||
// When loaded with colorPlane=0 (default), all 3 planes are set (val 7 = white composite)
|
||||
byte[] pixels = slot.getPixelData();
|
||||
assertEquals(7, pixels[0] & 0xFF);
|
||||
|
||||
// Render image
|
||||
BufferedImage img = slot.getImage(Color.WHITE, Color.BLACK);
|
||||
assertNotNull(img);
|
||||
|
||||
// Pixel (0,0) should be white composite (Red=255, Green=255, Blue=255)
|
||||
int rgb = img.getRGB(0, 0);
|
||||
Color pixelColor = new Color(rgb, true);
|
||||
assertEquals(255, pixelColor.getRed());
|
||||
assertEquals(255, pixelColor.getGreen());
|
||||
assertEquals(255, pixelColor.getBlue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormat3Bitstream() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
|
||||
// Format 3 payload
|
||||
// byte 0: 0x03 (Format 3)
|
||||
// byte 1: LCID (0x44)
|
||||
// byte 2: code point (0x43)
|
||||
// byte 3: RWS (2)
|
||||
// followed by (9 * 16 + 7) / 8 = 18 bytes
|
||||
byte[] payload = new byte[4 + 18];
|
||||
payload[0] = 0x03; // Format 3
|
||||
payload[1] = 0x44; // LCID 0x44
|
||||
payload[2] = 0x43; // Code point 0x43
|
||||
payload[3] = 0x02; // RWS 2
|
||||
payload[4] = (byte) 0x80; // First pixel is set
|
||||
|
||||
manager.loadps(payload);
|
||||
|
||||
ProgramSymbolSet set = manager.getSymbolSet(0x44);
|
||||
assertNotNull(set);
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x44, 0x43);
|
||||
assertNotNull(slot);
|
||||
assertEquals(1, slot.getPixelData()[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.lib3270j.tls;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lib3270j.ConnectionConfig;
|
||||
import org.lib3270j.TerminalModel;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class TlsConfigTest {
|
||||
|
||||
@Test
|
||||
public void testConnectionConfigDefaults() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
assertFalse(config.isUseTls());
|
||||
assertTrue(config.isTlsVerifyCert());
|
||||
assertEquals("TLS", config.getSslProtocol());
|
||||
assertNull(config.getCertificateVerifier());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseHostStringTlsPrefixes() {
|
||||
// L: prefix
|
||||
ConnectionConfig c1 = ConnectionConfig.parseHostString("L:mainframe.example.com", 0, TerminalModel.IBM_3279_4);
|
||||
assertTrue(c1.isUseTls());
|
||||
assertEquals("mainframe.example.com", c1.getHost());
|
||||
assertEquals(992, c1.getPort());
|
||||
|
||||
// ssl: prefix with explicit port
|
||||
ConnectionConfig c2 = ConnectionConfig.parseHostString("ssl:zos.local:2323", 0, TerminalModel.IBM_3279_2);
|
||||
assertTrue(c2.isUseTls());
|
||||
assertEquals("zos.local", c2.getHost());
|
||||
assertEquals(2323, c2.getPort());
|
||||
|
||||
// y: prefix
|
||||
ConnectionConfig c3 = ConnectionConfig.parseHostString("y:vm370.net:992", 23, TerminalModel.IBM_3279_4);
|
||||
assertTrue(c3.isUseTls());
|
||||
assertEquals("vm370.net", c3.getHost());
|
||||
assertEquals(992, c3.getPort());
|
||||
|
||||
// Plain host
|
||||
ConnectionConfig c4 = ConnectionConfig.parseHostString("plain.host.com", 23, TerminalModel.IBM_3279_4);
|
||||
assertFalse(c4.isUseTls());
|
||||
assertEquals("plain.host.com", c4.getHost());
|
||||
assertEquals(23, c4.getPort());
|
||||
|
||||
// IPv6 host with brackets
|
||||
ConnectionConfig c5 = ConnectionConfig.parseHostString("L:[2001:db8::1]:992", 0, TerminalModel.IBM_3279_4);
|
||||
assertTrue(c5.isUseTls());
|
||||
assertEquals("2001:db8::1", c5.getHost());
|
||||
assertEquals(992, c5.getPort());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTlsTrustManagerBypass() {
|
||||
ConnectionConfig config = new ConnectionConfig("untrusted.host", 992);
|
||||
config.setUseTls(true);
|
||||
config.setTlsVerifyCert(false);
|
||||
|
||||
TlsTrustManager trustManager = new TlsTrustManager(config);
|
||||
// With tlsVerifyCert=false, checkServerTrusted must not throw even with null/empty chain
|
||||
assertDoesNotThrow(() -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTlsTrustManagerVerifierCallback() {
|
||||
ConnectionConfig config = new ConnectionConfig("selfsigned.host", 992);
|
||||
config.setUseTls(true);
|
||||
config.setTlsVerifyCert(true);
|
||||
|
||||
AtomicBoolean verifierInvoked = new AtomicBoolean(false);
|
||||
config.setCertificateVerifier((chain, authType, exception) -> {
|
||||
verifierInvoked.set(true);
|
||||
return true; // Accept certificate
|
||||
});
|
||||
|
||||
TlsTrustManager trustManager = new TlsTrustManager(config);
|
||||
// Standard verification will fail on empty cert chain, triggering our callback
|
||||
assertDoesNotThrow(() -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA"));
|
||||
assertTrue(verifierInvoked.get(), "Expected verifier callback to be invoked");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTlsTrustManagerVerifierRejection() {
|
||||
ConnectionConfig config = new ConnectionConfig("badcert.host", 992);
|
||||
config.setUseTls(true);
|
||||
config.setTlsVerifyCert(true);
|
||||
|
||||
config.setCertificateVerifier((chain, authType, exception) -> false); // Reject certificate
|
||||
|
||||
TlsTrustManager trustManager = new TlsTrustManager(config);
|
||||
assertThrows(CertificateException.class, () -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateSSLContext() throws Exception {
|
||||
ConnectionConfig config = new ConnectionConfig("secure.host", 992);
|
||||
config.setUseTls(true);
|
||||
|
||||
SSLContext context = TlsTrustManager.createSSLContext(config);
|
||||
assertNotNull(context);
|
||||
assertNotNull(context.getSocketFactory());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user