package org.h3270.host;
/*
* Copyright (C) 2003-2008 akquinet tech@spree
*
* 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 3 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 org.apache.avalon.framework.configuration.Configuration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.h3270.render.H3270Configuration;
import java.io.BufferedReader;
import java.io.File;
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;
/**
* A Terminal that connects to the host via s3270.
*
* @author Andre Spiegel spiegel@gnu.org
* @version $Id: S3270.java,v 1.28 2008/11/21 14:47:22 spiegel Exp $
*/
public class S3270 implements Terminal {
private final static Log logger = LogFactory.getLog(S3270.class);
private String hostname = null;
private String logicalUnit = null;
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(String logicalUnit, String hostname, Configuration configuration) {
this.logicalUnit = logicalUnit;
this.hostname = hostname;
this.screen = new S3270Screen();
String commandLine = buildCommandLine(logicalUnit, hostname, configuration);
try {
logger.info("Starting s3270: " + 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 (IOException ex) {
throw new RuntimeException("IO Exception while starting s3270", ex);
}
}
/**
* Builds the command line for starting the s3270 process.
*
* @param hostname
* the name of the host to connect to.
* @param configuration
* the configuration for h3270
* @return a command line, ready to be executed by Runtime.exec()
*/
private String buildCommandLine(String logicalUnit, String hostname, Configuration configuration) {
String execPath = configuration.getChild("exec-path").getValue("/usr/local/bin");
Configuration s3270_options = configuration.getChild("s3270-options");
String charset = s3270_options.getChild("charset").getValue("bracket");
String model = s3270_options.getChild("model").getValue("3");
String additional = s3270_options.getChild("additional").getValue("");
File s3270_binary = new File(execPath, "s3270");
StringBuffer cmd = new StringBuffer(s3270_binary.toString());
cmd.append(" -model " + model);
if (!charset.equals("bracket")) {
cmd.append(" -charset " + charset);
}
if (additional.length() > 0) {
cmd.append(" " + additional);
}
cmd.append(" ");
if (logicalUnit != null) {
cmd.append(logicalUnit).append('@');
}
cmd.append(hostname);
return cmd.toString();
}
/**
* Represents the result of an s3270 command.
*/
private class Result {
public final List data;
public final String status;
public Result(List data, String status) {
this.data = data;
this.status = status;
}
}
/**
* Perform an s3270 command. All communication with s3270 should go via this method.
*/
private Result doCommand(String command) {
try {
out.println(command);
out.flush();
if (logger.isDebugEnabled()) {
logger.debug("---> " + command);
}
List lines = new ArrayList();
while (true) {
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);
}
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 (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 {
public String message = null;
@Override
public void run() {
BufferedReader err = new BufferedReader(new InputStreamReader(s3270.getErrorStream()));
try {
while (true) {
String msg = err.readLine();
if (msg == null) {
break;
}
message = msg;
}
} catch (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 (InterruptedException ex) {
}
try {
int exitValue = s3270.exitValue();
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 (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++) {
Result r = doCommand("");
if (r.status.startsWith("U F")) {
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
}
}
}
public void disconnect() {
out.println("quit");
out.flush();
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
if (s3270 != null) {
s3270.destroy();
}
} catch (InterruptedException ex) {
if (s3270 != null) {
s3270.destroy();
}
}
}
}).start();
try {
s3270.waitFor();
} catch (InterruptedException ex) { /* ignore */
}
try {
in.close();
} catch (IOException ex) { /* ignore */
}
out.close();
in = null;
out = null;
s3270 = null;
}
public boolean isConnected() {
if (s3270 == null || in == null || out == null) {
return false;
} else {
Result r = doCommand("");
if (r.status.matches(". . . C.*")) {
return true;
} else {
out.println("quit");
out.flush();
s3270.destroy();
s3270 = null;
in = null;
out = null;
return false;
}
}
}
public String getHostname() {
return hostname;
}
public String getLogicalUnit() {
return logicalUnit;
}
public void dumpScreen(String filename) {
screen.dump(filename);
}
/**
* Updates the screen object with s3270's buffer data.
*/
public void updateScreen() {
while (true) {
Result r = doCommand("readbuffer ascii");
if (r.data.size() > 0) {
String firstLine = (String)r.data.get(0);
if (firstLine.startsWith("data: Keyboard locked")) {
continue;
}
}
screen.update(r.status, r.data);
break;
}
}
public Screen getScreen() {
return screen;
}
/**
* Writes all changed fields back to s3270.
*/
public void submitScreen() {
for (Iterator i = screen.getFields().iterator(); i.hasNext();) {
Field f = (Field)i.next();
if ((f instanceof InputField) && ((InputField)f).isChanged()) {
doCommand("movecursor (" + f.getStartY() + ", " + f.getStartX() + ")");
doCommand("eraseeof");
String value = f.getValue();
for (int j = 0; j < value.length(); j++) {
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(String data) {
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++) {
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 newline() {
doCommand("newline");
waitFormat();
}
public void eraseEOF() {
doCommand("eraseEOF");
}
public void pa(int number) {
doCommand("pa(" + number + ")");
waitFormat();
}
public void pf(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})");
public void doKey(String key) {
Matcher m = FUNCTION_KEY_PATTERN.matcher(key);
if (m.matches()) { // function key
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 {
Class c = this.getClass();
Method method = c.getMethod(key, new Class[] {});
method.invoke(this, new Object[] {});
} catch (NoSuchMethodException ex) {
throw new IllegalArgumentException("no such key: " + key);
} catch (IllegalAccessException ex) {
throw new RuntimeException("illegal s3270 method access for key: " + key);
} catch (InvocationTargetException ex) {
throw new RuntimeException("error invoking s3270 for key: " + key + ", exception: " + ex.getTargetException());
}
}
}
@Override
public String toString() {
return "s3270 " + super.toString();
}
public static void main(String[] args) throws Exception {
Configuration configuration = H3270Configuration.create("/home/spiegel/projects/h3270/cvs/webapp/WEB-INF/h3270-config.xml");
S3270 s3270 = new S3270(null, "locis.loc.gov", configuration);
System.out.println(s3270.isConnected());
}
}