001/*
002 * Copyright (c) 2012 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: crawl $'
006 * '$Date: 2012-05-09 11:05:40 -0700 (Wed, 09 May 2012) $' 
007 * '$Revision: 29823 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029
030package org.sdm.spa.actors.transport;
031
032import java.io.ByteArrayOutputStream;
033import java.io.File;
034
035import org.apache.commons.logging.Log;
036import org.apache.commons.logging.LogFactory;
037import org.kepler.ssh.ExecException;
038import org.kepler.ssh.LocalExec;
039import org.kepler.ssh.SshExec;
040import org.sdm.spa.actors.transport.vo.ConnectionDetails;
041
042import ptolemy.kernel.util.IllegalActionException;
043
044/**
045 * This class provides functionality to copy files between two machine using
046 * SSH. Remote machines should be accessible using SSH to be able to copy files.
047 * This is a base class that can be extended by Protocol specific class that  
048 * generate different commands to perform the copy operation. 
049 * <p>
050 * Whether the copy operation would by default overwrite existing files depends
051 * on the actual protocol used for file copy. Optional command line options may
052 * be used to override the default behavior in some cases depending on the 
053 * protocol used. Refer to the documentation of specific sub class for details.
054 * <p>
055 * When both source and destination machines are local hosts, the protocol 
056 * specified is ignored and file copy is done using Java. 
057 * <p>
058 * When both source and destination are the same machine a simple cp command is
059 * used instead of any specific protocol
060 * @author Chandrika Sivaramakrishnan
061 *
062 */
063public abstract class FileCopierBase {
064  /////////////////Private Variables///////////////////////
065  private static final Log log = LogFactory.getLog(FileCopierBase.class.getName());
066  private static final boolean isDebugging = log.isDebugEnabled();
067  
068  /////////////////Protected variables ///////////////////
069  protected boolean forcedCleanup = false;
070  protected int timeout = 0;
071  protected String cmdLineOptions = "";
072  protected String protocolPathSrc = "";
073  protected String protocolPathDest = "";
074
075  //////////////////Protected Methods//////////////////
076  /**
077   * Copies files to destination path on the same machine.  
078   * @param srcFile - source file to be copied
079   * @param destFile - local path into which source should be copied 
080   * @param recursive - flag to indicate if directories should be copied recursively
081   * @return CopyResult object containing the exit code and error message if any.
082   * exit code 0 represents successful file transfer.
083   */
084  protected CopyResult copyLocal(String sourceFile, String destinationFile,
085      boolean recursive){
086    try {
087      int count = 0;
088      LocalExec exObject = new LocalExec();
089      File file = new File(sourceFile);
090      count = exObject.copyTo(file, destinationFile, recursive);
091      if(count>0){
092        return new CopyResult();
093      }else{
094        return new CopyResult(1,"No files where copied","");
095      }
096    }catch(ExecException e){
097        return new CopyResult(1,e.getMessage(),"");
098    }
099  }
100  
101  /**
102   * Connects to the remote machines and execute a 'cp' command to copy files
103   * to destination dir on the same remote machine
104   * @param srcFile - source file to be copied
105   * @param destFile - local path into which source should be copied 
106   * @param recursive - flag to indicate if directories should be copied recursively
107   * @return CopyResult object containing the exit code and error message if any.
108   * exit code 0 represents successful file transfer.
109   */
110  protected CopyResult copyLocalOnRemoteMachine(ConnectionDetails srcDetails,
111                        String srcFile, ConnectionDetails destDetails, String destFile,
112                        boolean recursive) throws ExecException {
113            
114            ByteArrayOutputStream cmdStdout = new ByteArrayOutputStream();
115            ByteArrayOutputStream cmdStderr = new ByteArrayOutputStream();
116            // Conenct to source by ssh
117            SshExec sshObject = new SshExec(srcDetails.getUser(), srcDetails.getHost(),
118                srcDetails.getPort());
119            sshObject.setTimeout(timeout, false, false);
120            sshObject.setForcedCleanUp(forcedCleanup);
121            
122            try
123            {
124              //no pseudo terminal is required as password is not required for cp
125              sshObject.setPseudoTerminal(false);
126              StringBuffer cmd = new StringBuffer(100);
127              int exitCode = 0;
128          
129              cmd.append("cp ");
130              if (recursive) {
131                cmd.append(" -r ");
132              } else {
133                cmd.append("  ");
134              }
135              cmd.append(srcFile);
136              cmd.append("  ");
137              cmd.append(destFile);
138          
139              if (isDebugging)
140                log.debug("remote copy cmd=" + cmd);
141              exitCode = sshObject.executeCmd(cmd.toString(), cmdStdout, cmdStderr,
142                  destDetails.toString());
143              log.debug("ExitCode:"+ exitCode);
144              log.debug("stdout:"+cmdStdout);
145              log.debug("stderr:"+cmdStderr);
146              
147              String message = cmdStderr.toString();
148              if(message==null || message.trim().equals("")){
149                  message = cmdStdout.toString();
150              }
151              return new CopyResult(exitCode,message,null);
152            }catch(Exception e){
153              return new CopyResult(1, e.getMessage(),null);
154            }
155  }
156
157  /**
158   * Copies file from a remote host to local machine
159   * 
160   * @param srcDetails - object containing the source machine connection details
161   * @param srcFile - source file to be copied
162   * @param destFile - local path into which source should be copied 
163   * @param recursive - flag to indicate if directories should be copied recursively
164   * @return CopyResult object containing the exit code and error message if any.
165   * exit code 0 represents successful file transfer.
166   * @throws ExecException
167   */
168  protected abstract CopyResult copyFrom(ConnectionDetails srcDetails, String srcFile, 
169                  String destFile, boolean recursive) throws ExecException;
170
171  /**
172   * Copies files from local machine to a remote host
173   * 
174   * @param srcFile - local source file to be copied
175   * @param destDetails - object containing the destination machine connection details
176   * @param destFile - path into which source should be copied 
177   * @param recursive - flag to indicate if directories should be copied recursively
178   * @return CopyResult object containing the exit code and error message if any.
179   * exit code 0 represents successful file transfer.
180   * @throws ExecException
181   */
182  protected abstract CopyResult copyTo(String srcFile, ConnectionDetails destDetails,
183      String destFile, boolean recursive) throws ExecException;
184
185  /**
186   * Copies files between two remote machines. 
187   * 
188   * @param srcDetails - object containing the source machine connection details
189   * @param srcFile - source file to be copied
190   * @param destDetails - object containing the destination machine connection details
191   * @param destFile - path into which source should be copied 
192   * @param recursive - flag to indicate if directory should be copied recursively
193   * @return CopyResult object containing the exit code and error message if any.
194   * exit code 0 represents successful file transfer.
195   * @throws ExecException
196   */
197  protected abstract CopyResult copyRemote(ConnectionDetails srcDetails,
198      String srcFile, ConnectionDetails destDetails, String destFile,
199      boolean recursive) throws ExecException;
200
201  /**
202   * Generic copy method that does the initial input validation and calls the
203   * copyTo, copyFrom or copyRemote of the appropriate FileCopier subclass.  
204   * Subclasses of FileCopier implement these methods based on the protocol that
205   * it uses for file copy. If both source and destination are local host, 
206   * ignores the protocol specified by the user and copies file using java
207   * <p> 
208   * @param srcDetails - ConnectionDetails object with source machine details
209   * @param srcFile - File to be copied
210   * @param destDetails - ConnectionDetails object with destination machine details
211   * @param destFile - Destination file or directory
212   * @param recursive - whether directory should be copied recursively
213   * @return exitCode
214   * @throws IllegalActionException
215   * @throws ExecException
216   */
217  protected CopyResult copy(ConnectionDetails srcDetails, String srcFile,
218      ConnectionDetails destDetails, String destFile, boolean recursive)
219      throws IllegalActionException, ExecException {
220
221    if (srcDetails.isLocal()) {
222      srcFile = handleRelativePath(srcFile);
223    }
224
225    if (destDetails.isLocal()) {
226      destFile = handleRelativePath(destFile);
227    }
228
229    if (srcDetails.getPort() == -1) {
230      srcDetails.setPort(getDefaultPort());
231    }
232    if (destDetails.getPort() == -1) {
233      destDetails.setPort(getDefaultPort());
234    }
235
236    if (isDebugging) {
237      log.debug("Source= " + srcDetails);
238      log.debug("Destination= " + destDetails);
239    }
240
241    //Both source and destination are local hosts
242    if (srcDetails.isLocal() && destDetails.isLocal()) {
243      return copyLocal(srcFile, destFile, recursive);
244    }
245    
246    //Either is a local host
247    if (srcDetails.isLocal()) {
248      // copy to remote destination
249      return copyTo(srcFile, destDetails, destFile, recursive);
250    } else if (destDetails.isLocal()) {
251      return copyFrom(srcDetails, srcFile, destFile, recursive);
252    }
253    
254    //Check if both the src and destination remote machines are same
255    if(srcDetails.getHost().equals(destDetails.getHost())){
256        return copyLocalOnRemoteMachine(srcDetails, srcFile, destDetails, destFile, recursive);
257    }
258    return copyRemote(srcDetails, srcFile, destDetails, destFile, recursive);
259  }
260
261  
262
263/**
264   * This is used to set the users PATH variable, if the user has not specified 
265   * the path where the protocol is installed. In such cases the 
266   * program will search for it in a default list of path. 
267   * @param cmd - command to execute for file copy 
268   * @return original command prefixed with command to set PATH variable. 
269   */
270  protected String getCmdWithDefaultPath(StringBuffer cmd) {
271    StringBuffer cmdWithPath = new StringBuffer(100);
272    cmdWithPath
273        .append("bash -c 'export PATH=/usr/bin:/bin:/usr/local/bin:~:.:$PATH; ");
274    cmdWithPath.append(cmd);
275    cmdWithPath.append("'");
276    return cmdWithPath.toString();
277  }
278
279  /**
280   * default port for the file transfer protocol. Child class should either
281   * return a specific port number or -1 if it doesn't want to enforce a 
282   * specific port number
283   *    */
284  protected abstract int getDefaultPort();
285  
286  //////////////////Private methods ///////////////////////////////////
287  
288private String handleRelativePath(String localfile){
289        String fileWithPath = localfile.trim();
290        
291        String userhome = System.getProperty("user.home");
292        //Work around for java 1.6 bug. Java doesn't return the correct
293        //user home directory on vista or windows 7. 
294        //http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6519127
295        if ( System.getProperty("os.name").toLowerCase().indexOf("win") >= 0 ) {
296                userhome = System.getenv().get("HOMEPATH");
297        } 
298        
299        if (localfile.contains(",")) {
300                log.debug("***** Detected file list *******");
301                String[] srcFile_list = localfile.split(",");
302                StringBuffer newlist = new StringBuffer();
303                for (int i = 0; i < srcFile_list.length; i++) {
304                        srcFile_list[i] = srcFile_list[i].trim();
305                        // Anand: The first predicate applies to Windows; the
306                        // selected offset assumes that
307                        // drive identifiers will be 1 character long.
308                        if (!(srcFile_list[i].startsWith(":\\", 1) || 
309                                        srcFile_list[i].startsWith(":/", 1) || 
310                                        srcFile_list[i].startsWith("/"))) {
311                                 newlist.append(userhome);
312                                 newlist.append(File.separatorChar);
313                        }
314                        newlist.append(srcFile_list[i]);
315                        newlist.append(",");
316                }
317                fileWithPath = newlist.toString();
318                if(fileWithPath.endsWith(",")){
319                        fileWithPath = fileWithPath.substring(0, fileWithPath.length() -1);
320                }
321                
322        } else {
323                // single file 
324                // The first predicate applies to Windows; the selected offset
325                // assumes that
326                // drive identifiers will be 1 character long.
327                // We can access file using relative path
328                if (!(fileWithPath.startsWith(":\\", 1) 
329                                || fileWithPath.startsWith(":/", 1) 
330                                || fileWithPath.startsWith("/"))) {
331                        fileWithPath = userhome + File.separatorChar + fileWithPath;
332                }
333        }
334
335        log.debug("From handleRelativePath - Returning "+fileWithPath);
336        return fileWithPath;
337}
338  
339  /////////////////Public getters and setters/////////////////////////
340  public boolean isForcedCleanup() {
341    return forcedCleanup;
342  }
343
344  public void setCleanup(boolean cleanup) {
345    this.forcedCleanup = cleanup;
346  }
347
348  public int getTimeout() {
349    return timeout;
350  }
351
352  public void setTimeout(int timeout) {
353    this.timeout = timeout;
354  }
355
356  public String getCmdLineOptions() {
357    return cmdLineOptions;
358  }
359
360  public void setCmdLineOptions(String cmdLineOptions) {
361    if (cmdLineOptions == null) {
362      this.cmdLineOptions = "";
363    } else {
364      this.cmdLineOptions = cmdLineOptions.trim();
365    }
366  }
367
368  public String getProtocolPathSrc() {
369    return protocolPathSrc;
370  }
371
372  public void setProtocolPathSrc(String protocolPathSrc) {
373    if (protocolPathSrc == null || protocolPathSrc.trim().equals("")) {
374      this.protocolPathSrc = "";
375    } else {
376      protocolPathSrc = protocolPathSrc.trim();
377      String seperator = "/";
378      if (protocolPathSrc.contains("\\")) {
379        seperator = "\\";
380      }
381      if (protocolPathSrc.endsWith(seperator)) {
382        this.protocolPathSrc = protocolPathSrc;
383      } else {
384        this.protocolPathSrc = protocolPathSrc + seperator;
385      }
386    }
387  }
388
389  public String getProtocolPathDest() {
390    return protocolPathDest;
391  }
392
393  public void setProtocolPathDest(String protocolPathDest) {
394    if (protocolPathDest == null || protocolPathDest.trim().equals("")) {
395      this.protocolPathDest = "";
396    } else {
397      String seperator = "/";
398      if (protocolPathDest.contains("\\")) {
399        seperator = "\\";
400      }
401      if (protocolPathDest.endsWith(seperator)) {
402        this.protocolPathDest = protocolPathDest;
403      } else {
404        this.protocolPathDest = protocolPathDest + seperator;
405      }
406    }
407  }
408  
409  //Inner class 
410  /**Object that contains the exit code and (error) message associated with the
411   *copy operation. Expects a exit code of 0 to denote successful file transfer.
412   *If exit code is zero, the error message is set to empty string
413   */
414  public class CopyResult {
415    private int exitCode;
416    private String errorMsg;
417    private String warningMsg;
418     
419    /**
420     * Default constructor. Represents a successful file transfer. 
421     * Defaults exit code to zero and message to empty string 
422     */
423    public CopyResult(){
424      exitCode = 0;
425      errorMsg = "";
426      warningMsg = "";
427    }
428   
429    public CopyResult(int exitCode, String message, String warningMessage){
430      this.exitCode = exitCode;
431      if(warningMessage == null){
432          warningMessage = "";
433      }
434      if(exitCode==0){
435        //operation successful
436        this.errorMsg ="";
437        this.warningMsg = warningMessage;
438      }else{
439        this.errorMsg = message;
440        this.warningMsg = warningMessage;
441      }
442    }
443    
444    @Override
445    public String toString(){
446      return exitCode+":"+errorMsg + ", warnings:" + warningMsg;
447    }
448
449    public int getExitCode() {
450      return exitCode;
451    }
452
453    public void setExitCode(int exitCode) {
454      this.exitCode = exitCode;
455    }
456
457    public String getErrorMsg() {
458      return errorMsg;
459    }
460
461    public void setErrorMsg(String errorMsg) {
462      this.errorMsg = errorMsg;
463    }
464
465    public String getWarningMsg() {
466      return warningMsg;
467    }
468
469    public void setWarningMsg(String warningMsg) {
470      this.warningMsg = warningMsg;
471    }
472  }
473
474}