package org.h3270.host;
/*
* Copyright (C) 2003-2006 akquinet framework solutions
*
* This file is part of h3270.
*
* h3270 is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* h3270 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with h3270; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.log4j.Logger;
/**
* A Terminal that connects to the host via s3270.
*
* @author Andre Spiegel spiegel@gnu.org
* @version $Id: S3270.java,v 1.26 2007/03/02 09:37:34 spiegel Exp $
*/
public class S3270 {
public enum TerminalMode {
MODE_80_24(2), MODE_80_32(3), MODE_80_43(4), MODE_132_27(5);
private int mode;
private TerminalMode(int mode) {
this.mode = mode;
}
public int getMode() {
return mode;
}
}
public enum TerminalType {
TYPE_3278("3278"), TYPE_3279("3279");
private String type;
private TerminalType(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
private static final Logger logger = Logger.getLogger(S3270.class);
private final String s3270Path;
private String hostname;
private final int port;
private final TerminalType type;
private final TerminalMode mode;
private S3270Screen screen = null;
/**
* The subprocess that does the actual communication with the host.
*/
private Process s3270 = null;
/**
* Used to send commands to the s3270 process.
*/
private PrintWriter out = null;
/**
* Used for reading input from the s3270 process.
*/
private BufferedReader in = null;
/**
* A thread that does a blocking read on the error stream from the s3270 process.
*/
private ErrorReader errorReader = null;
/**
* Constructs a new S3270 object. The s3270 subprocess (which does the communication with the host) is immediately
* started and connected to the target host. If this fails, the constructor will throw an appropriate exception.
*
* @param hostname
* the name of the host to connect to
* @param configuration
* the h3270 configuration, derived from h3270-config.xml
* @throws org.h3270.host.UnknownHostException
* if <code>hostname</code> cannot be resolved
* @throws org.h3270.host.HostUnreachableException
* if the host cannot be reached
* @throws org.h3270.host.S3270Exception
* for any other error not matched by the above
*/
public S3270(final String s3270Path, final String hostname, final int port, final TerminalType type,
final TerminalMode mode) {
this.s3270Path = s3270Path;
this.hostname = hostname;
this.port = port;
this.type = type;
this.mode = mode;
this.screen = new S3270Screen();
checkS3270PathValid(s3270Path);
final String commandLine = String.format("%s -model %s-%d %s:%d", s3270Path, type.getType(), mode.getMode(),
hostname, port);
try {
logger.info("starting " + commandLine);
s3270 = Runtime.getRuntime().exec(commandLine);
out = new PrintWriter(new OutputStreamWriter(s3270.getOutputStream(), "ISO-8859-1"));
in = new BufferedReader(new InputStreamReader(s3270.getInputStream(), "ISO-8859-1"));
errorReader = new ErrorReader();
errorReader.start();
waitFormat();
} catch (final IOException ex) {
throw new RuntimeException("IO Exception while starting s3270", ex);
}
}
private void checkS3270PathValid(String path) {
try {
Runtime.getRuntime().exec(path + " -v");
} catch (Exception e) {
throw new RuntimeException("could not find s3270 executable in the path");
}
}
private void assertConnected() {
if (s3270 == null) {
throw new RuntimeException("not connected");
}
}
public String getS3270Path() {
return s3270Path;
}
public String getHostname() {
return hostname;
}
public int getPort() {
return port;
}
public TerminalType getType() {
return type;
}
public TerminalMode getMode() {
return mode;
}
/**
* Represents the result of an s3270 command.
*/
private class Result {
private final List<String> data;
private final String status;
public Result(final List<String> data, final String status) {
this.data = data;
this.status = status;
}
public List<String> getData() {
return data;
}
public String getStatus() {
return status;
}
}
/**
* Perform an s3270 command. All communication with s3270 should go via this method.
*/
public Result doCommand(final String command) {
assertConnected();
try {
out.println(command);
out.flush();
if (logger.isDebugEnabled()) {
logger.debug("---> " + command);
}
final List<String> lines = new ArrayList<String>();
while (true) {
final String line = in.readLine();
if (line == null) {
checkS3270Process(); // will throw appropriate exception
// if we get here, it's a more obscure error
throw new RuntimeException("s3270 process not responding");
}
if (logger.isDebugEnabled()) {
logger.debug("<--- " + line);
}
if (line.equals("ok")) {
break;
}
lines.add(line);
}
final int size = lines.size();
if (size > 0) {
return new Result(lines.subList(0, size - 1), (String) lines.get(size - 1));
} else {
throw new RuntimeException("no status received in command: " + command);
}
} catch (final IOException ex) {
throw new RuntimeException("IOException during command: " + command, ex);
}
}
/**
* Performs a blocking read on the s3270 error stream. We do this asynchronously, because otherwise the error
* message might already be lost when we get a chance to look for it. The message is kept in the instance variable
* <code>message</code> for later retrieval.
*/
private class ErrorReader extends Thread {
private String message = null;
public void run() {
final BufferedReader err = new BufferedReader(new InputStreamReader(s3270.getErrorStream()));
try {
while (true) {
final String msg = err.readLine();
if (msg == null) {
break;
}
message = msg;
}
} catch (final IOException ex) {
// ignore
}
}
}
private static final Pattern unknownHostPattern = Pattern.compile(
// This message is hard-coded in s3270 as of version 3.3.5,
// so we can rely on it not being localized.
"Unknown host: (.*)");
private static final Pattern unreachablePattern = Pattern.compile(
// This is the hard-coded part of the error message in s3270 version 3.3.5.
"Connect to ([^,]+), port ([0-9]+): (.*)");
/**
* Checks whether the s3270 process is still running, and if it isn't, tries to determine the cause why it failed.
* This method throws an exception of appropriate type to indicate what went wrong.
*/
private void checkS3270Process() {
// Ideally, we'd like to call Process.waitFor() with a timeout,
// but that is so complicated to implement that we take a
// second-rate approach: wait a little while, and then check if
// the process is already terminated.
try {
Thread.sleep(100);
} catch (final InterruptedException ex) {
}
try {
final int exitValue = s3270.exitValue();
final String message = errorReader.message;
if (exitValue == 1 && message != null) {
Matcher m = unknownHostPattern.matcher(message);
if (m.matches()) {
throw new UnknownHostException(m.group(1));
} else {
m = unreachablePattern.matcher(message);
if (m.matches()) {
throw new HostUnreachableException(m.group(1), m.group(3));
}
}
throw new S3270Exception("s3270 terminated with code " + exitValue + ", message: "
+ errorReader.message);
}
} catch (final IllegalThreadStateException ex) {
// we get here if the process has still been running in the
// call to s3270.exitValue() above
throw new S3270Exception("s3270 not terminated, error: " + errorReader.message);
}
}
/**
* waits for a formatted screen
*/
private void waitFormat() {
for (int i = 0; i < 50; i++) {
final Result r = doCommand("");
if (r.getStatus().startsWith("U F")) {
return;
}
try {
Thread.sleep(100);
} catch (final InterruptedException ex) {
}
}
}
public void disconnect() {
assertConnected();
out.println("quit");
out.flush();
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
if (s3270 != null) {
s3270.destroy();
}
} catch (final InterruptedException ex) {
if (s3270 != null) {
s3270.destroy();
}
}
}
}).start();
try {
s3270.waitFor();
} catch (final InterruptedException ex) { /* ignore */
}
try {
in.close();
} catch (final IOException ex) { /* ignore */
}
out.close();
in = null;
out = null;
s3270 = null;
}
public boolean isConnected() {
if (s3270 == null || in == null || out == null) {
return false;
} else {
final Result r = doCommand("");
if (r.getStatus().matches(". . . C.*")) {
return true;
} else {
out.println("quit");
out.flush();
s3270.destroy();
s3270 = null;
in = null;
out = null;
return false;
}
}
}
public void dumpScreen(final String filename) {
assertConnected();
screen.dump(filename);
}
/**
* Updates the screen object with s3270's buffer data.
*/
public void updateScreen() {
assertConnected();
while (true) {
final Result r = doCommand("readbuffer ascii");
if (r.getData().size() > 0) {
final String firstLine = (String) r.getData().get(0);
if (firstLine.startsWith("data: Keyboard locked")) {
continue;
}
}
screen.update(r.getStatus(), r.getData());
break;
}
}
public Screen getScreen() {
assertConnected();
return screen;
}
/**
* Writes all changed fields back to s3270.
*/
public void submitScreen() {
assertConnected();
for (final Iterator<Field> i = screen.getFields().iterator(); i.hasNext();) {
final Field f = i.next();
if ((f instanceof InputField) && ((InputField) f).isChanged()) {
doCommand("movecursor (" + f.getStartY() + ", " + f.getStartX() + ")");
doCommand("eraseeof");
final String value = f.getValue();
for (int j = 0; j < value.length(); j++) {
final char ch = value.charAt(j);
if (ch == '\n') {
doCommand("newline");
} else if (!Integer.toHexString(ch).equals("0")) {
doCommand("key (0x" + Integer.toHexString(ch) + ")");
}
}
}
}
}
public void submitUnformatted(final String data) {
assertConnected();
int index = 0;
for (int y = 0; y < screen.getHeight() && index < data.length(); y++) {
for (int x = 0; x < screen.getWidth() && index < data.length(); x++) {
final char newCh = data.charAt(index);
if (newCh != screen.charAt(x, y)) {
doCommand("movecursor (" + y + ", " + x + ")");
if (!Integer.toHexString(newCh).equals("0")) {
doCommand("key (0x" + Integer.toHexString(newCh) + ")");
}
}
index++;
}
index++; // skip newline
}
}
// s3270 actions below this line
public void clear() {
doCommand("clear");
}
public void enter() {
doCommand("enter");
waitFormat();
}
public void tab() {
doCommand("tab");
}
public void newline() {
doCommand("newline");
waitFormat();
}
public void eraseEOF() {
doCommand("eraseEOF");
}
public void pa(final int number) {
doCommand("pa(" + number + ")");
waitFormat();
}
public void pf(final int number) {
doCommand("pf(" + number + ")");
waitFormat();
}
public void reset() {
doCommand("reset");
}
public void sysReq() {
doCommand("sysReq");
}
public void attn() {
doCommand("attn");
}
private static final Pattern FUNCTION_KEY_PATTERN = Pattern.compile("p(f|a)([0-9]{1,2})");
@SuppressWarnings("unchecked")
public void doKey(final String key) {
assertConnected();
final Matcher m = FUNCTION_KEY_PATTERN.matcher(key);
if (m.matches()) { // function key
final int number = Integer.parseInt(m.group(2));
if (m.group(1).equals("f")) {
this.pf(number);
} else {
this.pa(number);
}
} else if (key.equals("")) {
// use ENTER as a default action if the actual key got lost
this.enter();
} else { // other key: find a parameterless method of the same name
try {
final Class c = this.getClass();
final Method method = c.getMethod(key, new Class[] {});
method.invoke(this, new Object[] {});
} catch (final NoSuchMethodException ex) {
throw new IllegalArgumentException("no such key: " + key);
} catch (final IllegalAccessException ex) {
throw new RuntimeException("illegal s3270 method access for key: " + key);
} catch (final InvocationTargetException ex) {
throw new RuntimeException("error invoking s3270 for key: " + key + ", exception: "
+ ex.getTargetException());
}
}
}
}