org.h3270.host.S3270.java Source code

Java tutorial

Introduction

Here is the source code for org.h3270.host.S3270.java

Source

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.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

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.Map;
import java.util.Properties;
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;

    private int maxWait = 5000;

    /**
     * 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, Properties properties) {

        this.logicalUnit = logicalUnit;
        this.hostname = hostname;
        this.screen = new S3270Screen();

        String commandLine = buildCommandLine(logicalUnit, hostname, properties);
        try {
            ProcessBuilder pb = new ProcessBuilder(commandLine.split(" "));

            // If we are not on Windows, we can force s3270 to use UTF-8
            // encoding for screen dumps, so we are independent from
            // the system locale. On Windows, that doesn't work, so
            // we have to rely on the system code page being set to a
            // reasonable value.
            if (!System.getProperty("os.name").startsWith("Windows")) {
                Map<String, String> env = pb.environment();
                env.put("LANG", "en_US.UTF-8");
            }

            logger.info("Starting s3270: " + commandLine);
            s3270 = pb.start();

            String systemEncoding = System.getProperty("file.encoding");
            String encoding = systemEncoding != null ? systemEncoding : "ISO-8859-1";
            out = new PrintWriter(new OutputStreamWriter(s3270.getOutputStream(), encoding));
            in = new BufferedReader(new InputStreamReader(s3270.getInputStream(), encoding));
            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, Properties properties) {
        String execPath = getProperty(properties, "execPath", "/usr/local/bin");
        String charset = "cp" + properties.getProperty("host.codePage");
        String model = getProperty(properties, "charset", "3");
        String additional = getProperty(properties, "additional", null);
        File s3270_binary = new File(execPath, "s3270");
        StringBuffer cmd = new StringBuffer(s3270_binary.toString());
        cmd.append(" -model " + model);
        if (charset != null) {
            cmd.append(" -charset " + charset);
        }
        if (additional != null) {
            cmd.append(" " + additional);
        }
        // use UTF-8 encoding
        cmd.append(" -utf8");

        cmd.append(" ");

        if (logicalUnit != null) {
            cmd.append(logicalUnit).append('@');
        }

        // TODO - add more option according to
        cmd.append(hostname);
        return cmd.toString();
    }

    private String getProperty(Properties properties, String key, String defaultValue) {
        String value = properties.getProperty("s3270." + key);
        if (value != null) {
            return value;
        }
        return defaultValue;
    }

    /**
     * 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() {
        int totalWait = 0;
        // rm 26022014 - change wait to be configurable
        while (totalWait <= maxWait) {
            Result r = doCommand("");
            if (r.status.startsWith("U F")) {
                return;
            }
            try {
                Thread.sleep(100);
                totalWait += 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 (ch != 0) {
                        doCommand("key (" + encodeChar(ch) + ")");
                    }
                }
            }
        }
    }

    /**
     * Encode the given character as a Unicode code point (U+xxxx). Note: We intentionally do not handle code points outside BMP,
     * as there is no known code page on the host side that deals with them.
     */
    private String encodeChar(char ch) {
        return "U+" + 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 (newCh != 0) {
                        doCommand("key (" + encodeChar(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());
    }

    public void setCursor(int row, int column) {
        doCommand("movecursor (" + row + ", " + column + ")");
    }

    public void setMaxWait(int maxWait) {
        this.maxWait = maxWait;
    }
}