/*
** Module   : SFTPSession.java
** Abstract : SFTP support (connect, disconnect and common commands)
**
** Copyright (c) 2009-2021, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20090126 SFTP support (connect, disconnect and common commands).
** 002 CA  20210827 Refactored to allow other test types, beside terminal (which can be executed from the same 
**                  test suite).  Added SOAP and REST web service tests.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License
** along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.goldencode.harness.transport;

import java.util.*;
import com.jcraft.jsch.*;
import com.goldencode.harness.*;

/**
 * SFTP support (connect, disconnect and common commands).
 */
public class SFTPSession
{
   /** JSCH session object used to obtaining a channel. */
   private Session session = null;
   
   /** JSCH channel that provides streams over SFTP. */
   private ChannelSftp channel = null;
   
   /** Configuration data needed to access the target remote system. */
   private TerminalSessionSettings cfg = null;
   
   /**
    * Constructor.
    *
    * @param    cfg
    *           Specifies the configuration data needed to access the target
    *           remote system.
    */
   public SFTPSession(TerminalSessionSettings cfg)
   {
      this.cfg = cfg;
   }
   
   /**
    * Connect to the defined system/account and initialize a working
    * communications session.
    *
    * @throws   RuntimeException
    *           If any failure occurs.
    */
   public synchronized void connect()
   throws RuntimeException
   {
      if (channel == null)
      {
         try
         {
            JSch jsch = new JSch();
            
            session = jsch.getSession(cfg.userid, cfg.host, cfg.port);
            session.setPassword(cfg.password);
            session.setConfig("StrictHostKeyChecking", "no");
            session.connect(30000);
            
            channel = (ChannelSftp) session.openChannel("sftp");
            channel.connect();
         }
         
         catch (JSchException je)
         {
            // cleanup
            session = null;
            channel = null;
            
            throw new RuntimeException(je);
         }
      }
   }
   
   /**
    * Execute a <code>cd</code> command on the SFTP server.
    *
    * @param    dir
    *           The path to make the current directory.
    *
    * @throws   RuntimeException
    *           If any failure occurs.
    */
   public synchronized void cd(String dir)
   throws RuntimeException
   {
      boolean failed = true;
      
      try
      {
         channel.cd(dir);
         failed = false;
      }
      
      catch (SftpException se)
      {
         throw new RuntimeException(se);
      }
      
      finally
      {
         if (failed)
            disconnect();
      }
   }
   
   /**
    * Execute an <code>lcd</code> command for the SFTP session.
    *
    * @param    dir
    *           The path to make the current directory on the local system.
    *
    * @throws   RuntimeException
    *           If any failure occurs.
    */
   public synchronized void lcd(String dir)
   throws RuntimeException
   {
      boolean failed = true;
      
      try
      {
         channel.lcd(dir);
         failed = false;
      }
      
      catch (SftpException se)
      {
         throw new RuntimeException(se);
      }
      
      finally
      {
         if (failed)
            disconnect();
      }
   }
   
   /**
    * Execute a <code>pwd</code> command for the SFTP server.
    *
    * @return   The current directory on the remote system.
    *
    * @throws   RuntimeException
    *           If any failure occurs.
    */
   public synchronized String pwd()
   throws RuntimeException
   {
      boolean failed = true;
      
      String dir = null;
      
      try
      {
         dir = channel.pwd();
         failed = false;
      }
      
      catch (SftpException se)
      {
         throw new RuntimeException(se);
      }
      
      finally
      {
         if (failed)
            disconnect();
      }
      
      return dir;
   }
   
   /**
    * Execute an <code>lpwd</code> command for the SFTP session.
    *
    * @return   The current directory on the local system.
    *
    * @throws   RuntimeException
    *           If any failure occurs.
    */
   public synchronized String lpwd()
   throws RuntimeException
   {
      return channel.lpwd();
   }
   
   /**
    * Execute an <code>ls</code> command on the SFTP server.
    *
    * @param    dir
    *           The directory to query.
    *
    * @return   The directory details.
    *
    * @throws   RuntimeException
    *           If any failure occurs.
    */
   public synchronized String ls(String dir)
   throws RuntimeException
   {
      boolean failed = true;
      
      StringBuilder sb = new StringBuilder();
      
      try
      {
         Vector<ChannelSftp.LsEntry>   list = channel.ls(dir);
         Iterator<ChannelSftp.LsEntry> iter = list.iterator();
         
         while (iter.hasNext())
         {
            if (sb.length() > 0)
               sb.append("\n");
            
            ChannelSftp.LsEntry next = iter.next();
            sb.append(next.toString());
         }
         
         failed = false;
      }
      
      catch (SftpException se)
      {
         throw new RuntimeException(se);
      }
      
      finally
      {
         if (failed)
            disconnect();
      }
      
      return sb.toString();
   }
   
   /**
    * Execute a <code>get</code> command to copy a file from the SFTP server
    * to the local system.
    *
    * @param    remote
    *           The file or directory specification to copy from the server
    *           system. Global wildcard characters may be used in the
    *           specification.
    * @param    local
    *           The filename or directory which is the target on the local
    *           system.
    *
    * @throws   RuntimeException
    *           If any failure occurs.
    */
   public synchronized void get(String remote, String local)
   throws RuntimeException
   {
      boolean failed = true;
      
      try
      {
         channel.get(remote, local);
         failed = false;
      }
      
      catch (SftpException se)
      {
         throw new RuntimeException(se);
      }
      
      finally
      {
         if (failed)
            disconnect();
      }
   }
   
   /**
    * Execute a <code>put</code> command to copy a file from the local system
    * to the SFTP server.
    *
    * @param    local
    *           The file or directory specification to copy from the local
    *           system. Global wildcard characters may be used in the
    *           specification.
    * @param    remote
    *           The filename or directory which is the target on the server.
    *
    * @throws   RuntimeException
    *           If any failure occurs.
    */
   public synchronized void put(String local, String remote)
   throws RuntimeException
   {
      boolean failed = true;
      
      try
      {
         channel.put(local, remote);
         failed = false;
      }
      
      catch (SftpException se)
      {
         throw new RuntimeException(se);
      }
      
      finally
      {
         if (failed)
            disconnect();
      }
   }
   
   /**
    * Execute a <code>symlink</code> command to create symbolic link from one
    * file-system object to another. Both file-system objects are on the
    * SFTP server.
    *
    * @param    link
    *           The file or directory specification of the link (the new
    *           file system object).
    * @param    actual
    *           The filename or directory specification to which the link
    *           refers.
    *
    * @throws   RuntimeException
    *           If any failure occurs.
    */
   public synchronized void symlink(String link, String actual)
   throws RuntimeException
   {
      boolean failed = true;
      
      try
      {
         channel.symlink(link, actual);
         failed = false;
      }
      
      catch (SftpException se)
      {
         throw new RuntimeException(se);
      }
      
      finally
      {
         if (failed)
            disconnect();
      }
   }
   
   /**
    * Execute a <code>rename</code> command to change a file-system object's
    * name. This operation occurs on the SFTP server.
    *
    * @param    oldName
    *           The file or directory specification of the file system object
    *           to be changed.
    * @param    newName
    *           The filename or directory specification to which to change.
    *
    * @throws   RuntimeException
    *           If any failure occurs.
    */
   public synchronized void rename(String oldName, String newName)
   throws RuntimeException
   {
      boolean failed = true;
      
      try
      {
         channel.rename(oldName, newName);
         failed = false;
      }
      
      catch (SftpException se)
      {
         throw new RuntimeException(se);
      }
      
      finally
      {
         if (failed)
            disconnect();
      }
   }
   
   /**
    * Execute an <code>rm</code> command to remove a file-system object
    * from the SFTP server.
    *
    * @param    filespec
    *           The file specification of the object to be deleted. Wildcards
    *           may be used.
    *
    * @throws   RuntimeException
    *           If any failure occurs.
    */
   public synchronized void rm(String filespec)
   throws RuntimeException
   {
      boolean failed = true;
      
      try
      {
         channel.rm(filespec);
         failed = false;
      }
      
      catch (SftpException se)
      {
         // ignore missing files, otherwise fail
         if (se.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
         {
            failed = false;
         }
         else
         {
            throw new RuntimeException(se);
         }
      }
      
      finally
      {
         if (failed)
            disconnect();
      }
   }
   
   /**
    * Execute an <code>rmdir</code> command to remove a directory from the
    * SFTP server.
    *
    * @param    path
    *           The directory specification of the object to be deleted.
    *
    * @throws   RuntimeException
    *           If any failure occurs.
    */
   public synchronized void rmdir(String path)
   throws RuntimeException
   {
      boolean failed = true;
      
      try
      {
         channel.rmdir(path);
         failed = false;
      }
      
      catch (SftpException se)
      {
         throw new RuntimeException(se);
      }
      
      finally
      {
         if (failed)
            disconnect();
      }
   }
   
   /**
    * Execute a <code>mkdir</code> command to add a directory to the
    * SFTP server.
    *
    * @param    path
    *           The directory specification of the object to be created.
    *
    * @throws   RuntimeException
    *           If any failure occurs.
    */
   public synchronized void mkdir(String path)
   throws RuntimeException
   {
      boolean failed = true;
      
      try
      {
         channel.mkdir(path);
         failed = false;
      }
      
      catch (SftpException se)
      {
         throw new RuntimeException(se);
      }
      
      finally
      {
         if (failed)
            disconnect();
      }
   }
   
   /**
    * Disconnect the currently working communications session.
    */
   public synchronized void disconnect()
   {
      if (channel != null)
      {
         channel.disconnect();
         channel = null;
      }
      
      if (session != null)
      {
         session.disconnect();
         session = null;
      }
   }
}
