org.oscarehr.hospitalReportManager.SFTPConnector.java Source code

Java tutorial

Introduction

Here is the source code for org.oscarehr.hospitalReportManager.SFTPConnector.java

Source

/**
 * Copyright (c) 2008-2012 Indivica Inc.
 *
 * This software is made available under the terms of the
 * GNU General Public License, Version 2, 1991 (GPLv2).
 * License details are available via "indivica.ca/gplv2"
 * and "gnu.org/licenses/gpl-2.0.html".
 */

package org.oscarehr.hospitalReportManager;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.logging.FileHandler;
import java.util.logging.Logger;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.lang.StringUtils;
import org.oscarehr.util.LoggedInInfo;
import org.oscarehr.util.MiscUtils;

import oscar.OscarProperties;
import oscar.oscarMessenger.data.MsgProviderData;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;

/**
 * SFTP Connector to interact with servers and return the server's reply/file data.
 */
public class SFTPConnector {

    private static org.apache.log4j.Logger logger = MiscUtils.getLogger();

    private JSch jsch;
    private ChannelSftp cmd;
    private Session sess;
    private Logger fLogger; //file logger

    private static final String TEST_DIRECTORY = "Test";

    private static final String OMD_HRM_USER = OscarProperties.getInstance().getProperty("OMD_HRM_USER");
    private static final String OMD_HRM_IP = OscarProperties.getInstance().getProperty("OMD_HRM_IP");
    private static final int OMD_HRM_PORT = Integer
            .parseInt(OscarProperties.getInstance().getProperty("OMD_HRM_PORT"));

    //this file needs chmod 444 permissions for the connection to go through
    public static String OMD_directory = OscarProperties.getInstance().getProperty("OMD_directory");
    private static String OMD_keyLocation = OMD_directory
            + OscarProperties.getInstance().getProperty("OMD_HRM_AUTH_KEY_FILENAME");
    public static final String XSD_ontariomd = OMD_directory + "ontariomd_cds_dt.xsd";
    public static final String XSD_reportmanager = OMD_directory + "report_manager_cds.xsd";

    //where all the daily logs will be saved
    public final String logDirectory = OscarProperties.getInstance().getProperty("OMD_log_directory");

    //root folder for daily downloads
    public static String downloadsDirectory = OscarProperties.getInstance().getProperty("OMD_downloads");

    public final String fileDirectory = OscarProperties.getInstance().getProperty("OMD_stored");

    //set when initialized, to change keys, manually do it in the main constructor
    public static String decryptionKey = null;

    /**
     * String is the providerNo of people who don't want to see anymore messages.
     * This is cleared each time the run succeeds as it would be a new outtage after one success. 
     */
    private static HashSet<String> doNotSentMsgForOuttage = new HashSet<String>();

    /**
     * Default constructor instantiates the SFTP Connector for OMD.
     * 
     * Default use of this class is internally through Tomcat. See other constructor for running this class from the
     * command line.
     * 
     * @throws Exception
     */
    public SFTPConnector() throws Exception {
        this(OMD_HRM_IP, OMD_HRM_PORT, OMD_HRM_USER, getOMD_keyLocation());
    }

    /**
     * Instantiate this class and start to automatically download the specified folder, then delete contents. Revise as
     * necessary for order of operations.
     * 
     * @param remoteDir
     * @throws Exception
     */
    public SFTPConnector(String remoteDir) throws Exception {
        this();

        if (remoteDir == null) {
            return;
        }

        //get a list of files of remote directory
        String[] files = ls(remoteDir);

        //if no files in directory, got nothing to do
        if (files.length == 0) {
            fLogger.info("Server folder '" + remoteDir + "' has no files for downloading. Terminated.");
            throw new Exception("Server directory '" + remoteDir + "' has no files to download!");
        }

        //fetch all files from remote dir
        String[] localFilePaths = downloadDirectoryContents(remoteDir);

        String[] paths = null;
        if (doDecrypt()) {
            paths = decryptFiles(localFilePaths);
        } else {
            paths = localFilePaths;
        }

        //delete all files from remote dir
        deleteDirectoryContents(remoteDir, files);

        //disconnect
        close();
    }

    /**
     * Main constructor. To change keys, manually set the references below.
     * 
     * @param host
     * @param port
     * @param user
     * @param keyLocation
     * @throws Exception
     */
    public SFTPConnector(String host, int port, String user, String keyLocation) throws Exception {

        logger.debug("Host " + host + " port " + port + " user " + user + " keyLocation " + keyLocation);

        //daily log file name follows "day month year . log" (with no spaces)
        String logName = SFTPConnector.getDayMonthYearTimestamp() + ".log";
        String fullLogPath = this.logDirectory + logName;

        //prepare the logger
        FileHandler handler = new FileHandler(fullLogPath, true); //append log to daily log files
        fLogger = Logger.getLogger("SFTPConnector");
        fLogger.addHandler(handler);

        jsch = new JSch();

        jsch.addIdentity(keyLocation);
        sess = jsch.getSession(user, host, port);

        java.util.Properties confProp = new java.util.Properties();
        confProp.put("StrictHostKeyChecking", "no");
        sess.setConfig(confProp);

        sess.connect();

        Channel channel = sess.openChannel("sftp");
        channel.connect();
        cmd = (ChannelSftp) channel;
        fLogger.info("SFTP connection established with " + host + ":" + port + ". Current path on server is: "
                + cmd.pwd());
    }

    public static String getOMD_keyLocation() {
        return OMD_keyLocation;

    }

    public static void setOMD_keyLocation(String oMD_keyLocation) {
        OMD_keyLocation = oMD_keyLocation;
    }

    public static String getDownloadsDirectory() {
        String dd = downloadsDirectory;
        if (dd == null || dd.equals("")) {
            dd = "webapps/OscarDocument/hrm/sftp_downloads/";
            return dd;

        } else {
            return downloadsDirectory;
        }

    }

    public static void setDownloadsDirectory(String downloadsDir) {
        downloadsDirectory = downloadsDir;
    }

    public static String getDecryptionKey() {
        return decryptionKey;
    }

    public static void setDecryptionKey(String decryptKey) {
        decryptionKey = decryptKey;
    }

    /**
     * Ensure the specified folder exists within the SFTP download folder. If folder is null, then ensure that the
     * download folder exists.
     * 
     * @throws Exception
     */
    private static String prepareForDownload(String folder) throws Exception {

        //ensure the downloads directory exists
        String path = checkFolder(downloadsDirectory);

        //if it's a simple "do i have my downloads folder" check, then we're done!
        //no other folder is specified
        if (folder == null)
            return path;

        //if code gets to here then we're ensuring that specified folder exists within SFTP download folder.
        //-also fixes the beginning if the specified folder already begins with a '/' slash it ignores the slash
        String dir = downloadsDirectory + (folder == null ? ""
                : (folder.charAt(0) == '/' ? folder.substring(1, folder.length() - 1) : folder));

        //return the full path of the existing folder
        return checkFolder(dir);
    }

    /**
     * ls print - issue an 'ls' command and simply print the results to System out (rather than returning a String array
     * of elements listed from command)
     * 
     * @param folder
     * @throws SftpException
     */
    public void lsP(String folder) throws SftpException {
        ls(folder, true);
    }

    /**
     * Issue an 'ls' command and return the objects in an array
     * 
     * @param folder
     * @return
     * @throws SftpException
     */
    public String[] ls(String folder) throws SftpException {
        return ls(folder, false);
    }

    /**
     * Issue an 'ls' command on remote server and exclussively print the values or return them in a String array.
     * 
     * @param folder
     *            to issue the 'ls' command on
     * @param printInfo
     * @return
     * @throws SftpException
     */
    public String[] ls(String folder, boolean printInfo) throws SftpException {
        List fileList = cmd.ls(folder);
        String[] filenames = null;

        if (fileList != null) {

            //only instantiate array to hold ls results if user is not printing info
            if (!printInfo) {
                filenames = new String[fileList.size()];
            }

            logger.debug("ls " + folder);
            int i = 0;
            for (Object obj : fileList) {

                if (obj instanceof com.jcraft.jsch.ChannelSftp.LsEntry) {
                    LsEntry lsEntry = (LsEntry) obj;

                    //either print or store each element
                    if (printInfo) {
                        logger.debug(lsEntry.getFilename());
                    } else {
                        String fn = lsEntry.getFilename(); //filename
                        if (fn != null && !fn.equals(".") && !fn.equals("..")) {
                            filenames[i++] = fn;
                        }
                    }
                }
            }
        }

        return filenames;
    }

    /**
     * Download the contents of the specified directory on the server side. The presumption is made for OMD where the
     * user has to fetch all contents from the user's home directory. Thus a "./" is prepended to each folder requested
     * on the server. If you don't prepend the "./" before the folder directory, JSch will use the "/" directory (root
     * dir).
     * 
     * @param serverDirectory
     *            directory on server side to fetch contents
     * @param localDownloadFolder
     *            name of folder to place downloaded files. This folder is placed under the tmp folder specified at
     * @throws Exception
     *             custom error messages if Java is unable to create a folder in /tmp/oscar-sftp and parent dirs
     * @return array of full path filenames
     */
    public String[] downloadDirectoryContents(String serverDirectory) throws Exception {
        //get the filenames of all files in the directory
        String[] files = ls(serverDirectory);
        String[] fullPathFilenames = new String[files.length];

        //go into the server directory
        cmd.cd("./" + serverDirectory);
        //and fetch each file into the source folder

        String todaysFolderName = SFTPConnector.getDayMonthYearTimestamp();

        //ensure today's folder exists
        String fullPath = prepareForDownload(todaysFolderName);

        fLogger.info("About to download all contents of directory: " + serverDirectory);
        if (files.length == 0) {
            fLogger.info("No files to download from server folder: " + serverDirectory);
            return null;
        }

        int i = 0;
        //not too sure whether multiple connections are handled by the JSch library
        //such that multiple calls to "get" has a sync limit until one or more other
        //files have finished downloading.
        for (String file : files) {
            if (file != null) {
                String fullFilePath = fullPath + file;
                cmd.get(file, fullFilePath);
                fullPathFilenames[i++] = fullFilePath;
                fLogger.info("Downloaded File: " + fullFilePath);
                logger.debug("SFTP::Downloaded file: " + fullFilePath);
            }
        }

        return fullPathFilenames;
    }

    /**
     * Given a server-side directory, go in and delete all files
     * 
     * @param serverDirectory
     * @throws SftpException
     */
    public void deleteDirectory(String serverDirectory) throws SftpException {
        String[] files = ls(serverDirectory);
        deleteDirectoryContents(serverDirectory, files);
    }

    /**
     * Given a directory and the filenames of the directories (already pre-determined) go in the directory and delete
     * each file.
     * 
     * @param serverDirectory
     *            the directory onto which to remove contents
     * @param filenames
     *            a String array of filenames of the directory, pre-fetched specifically for the directory.
     * @throws SftpException
     */
    public void deleteDirectoryContents(String serverDirectory, String[] filenames) throws SftpException {
        cmd.cd("/");

        fLogger.info("About to delete all contents from server directory: " + serverDirectory);
        logger.debug("Deleting contents from directory: " + serverDirectory);
        cmd.cd(serverDirectory);
        for (String file : filenames) {
            if (file != null) {
                cmd.rm(file);

                fLogger.info("Deleted file " + file + " from server");
                logger.debug("Deleted server file " + file);
            }
        }

    }

    /**
     * Given a String array of absolute filenames of encrypted files, proceed to decrypt them in today's folder under
     * the specified folder below.
     * 
     * @param fullPaths
     * @throws Exception
     */
    public static String[] decryptFiles(String[] fullPaths) throws Exception {
        if (fullPaths.length == 0)
            return null;

        String[] decryptedFilePaths = new String[fullPaths.length];

        //placed under each daily's folder for all files needing decryption to store the decrypted version
        String decryptedFolderName = "decrypted";
        //ensure that the given folder exists (by trying to create it if it dne)
        //return the full path with last slash always there
        String saveToDirectoryFullPath = prepareForDownload(getDayMonthYearTimestamp() + "/" + decryptedFolderName);

        //we'll get each file's contents in a string then dump that onto a file
        String decryptedContent = null;
        String filename = "";

        FileWriter handler = null;
        BufferedWriter out = null;
        for (int x = 0; x < fullPaths.length; x++) {
            String sfile = fullPaths[x];
            if (sfile == null)
                continue;

            try {
                decryptedContent = decryptFile(sfile);
                filename = sfile.substring(sfile.lastIndexOf("/"));
                String newFullPath = saveToDirectoryFullPath + filename;
                handler = new FileWriter(newFullPath);
                out = new BufferedWriter(handler);
                out.write(decryptedContent);
                decryptedFilePaths[x] = newFullPath;
            } catch (Exception e) {
                //Don't want this to fail on all other files in the directory just because one doesn't decrypt;
                logger.error("Error decrypting file - " + sfile);
                decryptedFilePaths[x] = null;
            } finally {
                if (out != null)
                    out.close();
                if (handler != null)
                    handler.close();
            }
        }

        return decryptedFilePaths;
    }

    /**
     * Given the absolute path of an encrypted file, decrypt the file using the specified AES key at the top. Return the
     * string value of the decrypted file.
     * 
     * @param fullPath
     * @return
     * @throws Exception
     */
    public static String decryptFile(String fullPath) throws Exception {
        logger.debug("About to decrypt: " + fullPath);
        File encryptedFile = new File(fullPath);
        if (!encryptedFile.exists()) {
            throw new Exception("Could not find file '" + fullPath + "' to decrypt.");
        }

        //get the bytes of the file in an array
        int fileLength = (int) encryptedFile.length();
        byte[] fileInBytes = new byte[fileLength];
        FileInputStream fin = new FileInputStream(encryptedFile);
        try {
            fin.read(fileInBytes);
        } finally {
            fin.close();
        }

        //the provided key is 32 characters long string hex representation of a 128 hex key, get the 128-bit hex bytes
        byte keyBytes[] = toHex(OscarProperties.getInstance().getProperty("OMD_HRM_DECRYPTION_KEY"));

        SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");

        int maxKeyLen = Cipher.getMaxAllowedKeyLength("AES");

        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding", "SunJCE");

        cipher.init(Cipher.DECRYPT_MODE, key);

        byte[] decode = cipher.doFinal(fileInBytes);

        return new String(decode);
    }

    /**
     * Close channels, disconnect sessions, release/close file handlers.
     */
    public void close() {
        cmd.exit();
        sess.disconnect();
        fLogger.getHandlers()[0].close();
    }

    /********************************************************/
    /////////////////// HELPERS / STATIC /////////////////////
    /********************************************************/

    public static String getDayMonthYearTimestamp() {
        Calendar cal = Calendar.getInstance();

        String day = cal.get(Calendar.DAY_OF_MONTH) + "";
        if (day.length() == 1)
            day = "0" + day;

        String month = (cal.get(Calendar.MONTH) + 1) + "";
        if (month.length() == 1)
            month = "0" + month;

        String year = cal.get(Calendar.YEAR) + "";

        return day + month + year;
    }

    /**
     * Check that the given folder exists, if it doesn't exist, create it. Object method for convenience.
     * 
     * @param fullPath
     * @throws Exception
     */
    private static String checkFolder(String fullPath) throws Exception {
        return SFTPConnector.ensureFolderExists(fullPath);
    }

    /**
     * Ensure that the given folder exists by creating it if it isn't present.
     * 
     * Static method so other external Classes may use this feature.
     * 
     * @param fullPath
     * @throws Exception
     */
    public static String ensureFolderExists(String fullPath) throws Exception {
        File tmpFolder = new File(fullPath);
        if (!tmpFolder.exists()) {
            boolean res = tmpFolder.mkdir();
            if (!res)
                throw new Exception("Unable to create folder " + tmpFolder.getAbsolutePath()
                        + " required for SFTP operations. Please check permissions.");
        }

        return tmpFolder.getAbsolutePath() + "/";
    }

    public static byte[] toHex(String encoded) {
        if ((encoded.length() % 2) != 0)
            throw new IllegalArgumentException("Input string must contain an even number of characters");

        final byte result[] = new byte[encoded.length() / 2];
        final char enc[] = encoded.toCharArray();
        for (int i = 0; i < enc.length; i += 2) {
            StringBuilder curr = new StringBuilder(2);
            curr.append(enc[i]).append(enc[i + 1]);
            result[i / 2] = (byte) Integer.parseInt(curr.toString(), 16);
        }
        return result;

    }

    /********************************************************/
    //////////////////Auto Fetcher////////////////////////
    /**
     * @throws Exception
     ******************************************************/

    private static boolean isAutoFetchRunning = false;

    public static boolean isFetchRunning() {
        return SFTPConnector.isAutoFetchRunning;
    }

    public static boolean doDecrypt() {
        boolean decrypt = false;
        String decryptionKey = OscarProperties.getInstance().getProperty("OMD_HRM_DECRYPTION_KEY");

        if (StringUtils.isNotEmpty(decryptionKey)) {
            decrypt = true;
        }
        return decrypt;
    }

    public static synchronized void startAutoFetch(LoggedInInfo loggedInInfo) {

        if (!isAutoFetchRunning) {
            SFTPConnector.isAutoFetchRunning = true;
            logger.debug("Going into OMD to fetch auto data");

            String remoteDir = OscarProperties.getInstance().getProperty("OMD_HRM_REMOTE_DIR");

            if (remoteDir == null || remoteDir.isEmpty()) {
                remoteDir = TEST_DIRECTORY;
            }

            logger.info("SFTPConnector, remoteDir:" + remoteDir);

            try {
                logger.debug("Instantiating a new SFTP connection.");
                SFTPConnector sftp = new SFTPConnector();
                logger.debug("new SFTP connection established");

                String[] localFilePaths = null;

                try {
                    localFilePaths = sftp.downloadDirectoryContents(remoteDir);
                } finally {
                    sftp.close();
                }

                String[] paths = null;
                if (doDecrypt()) {
                    paths = decryptFiles(localFilePaths);
                } else {
                    paths = localFilePaths;
                }

                for (String filePath : paths) {
                    HRMReport report = HRMReportParser.parseReport(loggedInInfo, filePath);
                    if (report != null)
                        HRMReportParser.addReportToInbox(loggedInInfo, report);
                }

                logger.debug("Closed SFTP connection");
                logger.debug("Clearing doNotSend list");
                doNotSentMsgForOuttage.clear();
            } catch (Exception e) {
                logger.error("Couldn't perform SFTP fetch for HRM - notifying user of failure", e);
                notifyHrmError(loggedInInfo, e.getMessage());
            }

            SFTPConnector.isAutoFetchRunning = false;
        } else {
            logger.warn(
                    "There is currently an HRM fetch running -- will not run another until it has completed or timed out.");
        }
    }

    protected static void notifyHrmError(LoggedInInfo loggedInInfo, String errorMsg) {
        HashSet<String> sendToProviderList = new HashSet<String>();

        String providerNoTemp = "999998";
        if (!doNotSentMsgForOuttage.contains(providerNoTemp))
            sendToProviderList.add(providerNoTemp);

        if (loggedInInfo != null && loggedInInfo.getLoggedInProvider() != null) {
            // manual prompts always send to admin
            sendToProviderList.add(providerNoTemp);

            providerNoTemp = loggedInInfo.getLoggedInProviderNo();
            if (!doNotSentMsgForOuttage.contains(providerNoTemp))
                sendToProviderList.add(providerNoTemp);
        }

        // no one wants to hear about the problem
        if (sendToProviderList.size() == 0)
            return;

        String message = "OSCAR attempted to perform a fetch of HRM data at " + new Date()
                + " but there was an error during the task.\n\nSee below for the error message:\n" + errorMsg;

        oscar.oscarMessenger.data.MsgMessageData messageData = new oscar.oscarMessenger.data.MsgMessageData();

        ArrayList<MsgProviderData> sendToProviderListData = new ArrayList<MsgProviderData>();
        for (String providerNo : sendToProviderList) {
            MsgProviderData mpd = new MsgProviderData();
            mpd.providerNo = providerNo;
            mpd.locationId = "145";
            sendToProviderListData.add(mpd);
        }

        String sentToString = messageData.createSentToString(sendToProviderListData);
        messageData.sendMessage2(message, "HRM Retrieval Error", "System", sentToString, "-1",
                sendToProviderListData, null, null);
    }

    /**
     * adds the currently logged in user to the do not send anymore messages for this outtage list 
     */
    public static void addMeToDoNotSendList(LoggedInInfo loggedInInfo) {
        if (loggedInInfo != null && loggedInInfo.getLoggedInProvider() != null) {
            doNotSentMsgForOuttage.add(loggedInInfo.getLoggedInProviderNo());
        }
    }
}