Coverage Report - org.mule.transport.ftp.FtpConnector
 
Classes in this File Line Coverage Branch Coverage Complexity
FtpConnector
67%
110/163
40%
27/68
2.4
FtpConnector$1
62%
5/8
50%
1/2
2.4
 
 1  
 /*
 2  
  * $Id: FtpConnector.java 12377 2008-07-17 15:59:53Z tcarlson $
 3  
  * --------------------------------------------------------------------------------------
 4  
  * Copyright (c) MuleSource, Inc.  All rights reserved.  http://www.mulesource.com
 5  
  *
 6  
  * The software in this package is published under the terms of the CPAL v1.0
 7  
  * license, a copy of which has been included with this distribution in the
 8  
  * LICENSE.txt file.
 9  
  */
 10  
 
 11  
 package org.mule.transport.ftp;
 12  
 
 13  
 import org.mule.api.MuleException;
 14  
 import org.mule.api.MuleMessage;
 15  
 import org.mule.api.MuleRuntimeException;
 16  
 import org.mule.api.endpoint.EndpointURI;
 17  
 import org.mule.api.endpoint.ImmutableEndpoint;
 18  
 import org.mule.api.endpoint.InboundEndpoint;
 19  
 import org.mule.api.lifecycle.InitialisationException;
 20  
 import org.mule.api.service.Service;
 21  
 import org.mule.api.transport.ConnectorException;
 22  
 import org.mule.api.transport.DispatchException;
 23  
 import org.mule.api.transport.MessageReceiver;
 24  
 import org.mule.config.i18n.CoreMessages;
 25  
 import org.mule.config.i18n.MessageFactory;
 26  
 import org.mule.model.streaming.CallbackOutputStream;
 27  
 import org.mule.transport.AbstractConnector;
 28  
 import org.mule.transport.file.FilenameParser;
 29  
 import org.mule.transport.file.SimpleFilenameParser;
 30  
 import org.mule.util.ClassUtils;
 31  
 
 32  
 import java.io.IOException;
 33  
 import java.io.OutputStream;
 34  
 import java.text.MessageFormat;
 35  
 import java.util.ArrayList;
 36  
 import java.util.HashMap;
 37  
 import java.util.Iterator;
 38  
 import java.util.List;
 39  
 import java.util.Map;
 40  
 
 41  
 import org.apache.commons.net.ftp.FTPClient;
 42  
 import org.apache.commons.net.ftp.FTPFile;
 43  
 import org.apache.commons.pool.ObjectPool;
 44  
 import org.apache.commons.pool.impl.GenericObjectPool;
 45  
 
 46  32
 public class FtpConnector extends AbstractConnector
 47  
 {
 48  
 
 49  
     public static final String FTP = "ftp";
 50  
 
 51  
     // endpoint properties
 52  
     public static final String PROPERTY_POLLING_FREQUENCY = "pollingFrequency"; // inbound only
 53  
     public static final int DEFAULT_POLLING_FREQUENCY = 1000;
 54  
     public static final String PROPERTY_OUTPUT_PATTERN = "outputPattern"; // outbound only
 55  
     public static final String PROPERTY_PASSIVE_MODE = "passive";
 56  
     public static final String PROPERTY_BINARY_TRANSFER = "binary";
 57  
 
 58  
     // message properties
 59  
     public static final String PROPERTY_FILENAME = "filename";
 60  
 
 61  
 
 62  
     /**
 63  
      *  TODO it makes sense to have a type-safe adapter for FTP specifically, but without
 64  
      *  Java 5's covariant return types the benefits are diminished. Keeping it simple for now.
 65  
      */
 66  
     public static final String DEFAULT_FTP_CONNECTION_FACTORY_CLASS = "org.mule.transport.ftp.FtpConnectionFactory";
 67  
 
 68  
     /**
 69  
      * Time in milliseconds to poll. On each poll the poll() method is called
 70  
      */
 71  
     private long pollingFrequency;
 72  
 
 73  
     private String outputPattern;
 74  
 
 75  32
     private FilenameParser filenameParser = new SimpleFilenameParser();
 76  
 
 77  32
     private boolean passive = true;
 78  
 
 79  32
     private boolean binary = true;
 80  
 
 81  
     /**
 82  
      * Whether to test FTP connection on each take from pool.
 83  
      */
 84  32
     private boolean validateConnections = true;
 85  
 
 86  32
     private Map pools = new HashMap();
 87  
 
 88  32
     private String connectionFactoryClass = DEFAULT_FTP_CONNECTION_FACTORY_CLASS;
 89  
 
 90  
     public String getProtocol()
 91  
     {
 92  78
         return FTP;
 93  
     }
 94  
 
 95  
     public MessageReceiver createReceiver(Service service, InboundEndpoint endpoint) throws Exception
 96  
     {
 97  10
         List args = getReceiverArguments(endpoint.getProperties());
 98  10
         return serviceDescriptor.createMessageReceiver(this, service, endpoint, args.toArray());
 99  
     }
 100  
 
 101  
     protected List getReceiverArguments(Map endpointProperties)
 102  
     {
 103  10
         List args = new ArrayList();
 104  
         
 105  10
         long polling = getPollingFrequency();
 106  10
         if (endpointProperties != null)
 107  
         {
 108  
             // Override properties on the endpoint for the specific endpoint
 109  10
             String tempPolling = (String) endpointProperties.get(PROPERTY_POLLING_FREQUENCY);
 110  10
             if (tempPolling != null)
 111  
             {
 112  2
                 polling = Long.parseLong(tempPolling);
 113  
             }
 114  
         }
 115  10
         if (polling <= 0)
 116  
         {
 117  0
             polling = DEFAULT_POLLING_FREQUENCY;
 118  
         }
 119  10
         logger.debug("set polling frequency to " + polling);
 120  10
         args.add(new Long(polling));
 121  
         
 122  10
         return args;
 123  
     }
 124  
     
 125  
     /**
 126  
      * @return Returns the pollingFrequency.
 127  
      */
 128  
     public long getPollingFrequency()
 129  
     {
 130  12
         return pollingFrequency;
 131  
     }
 132  
 
 133  
     /**
 134  
      * @param pollingFrequency The pollingFrequency to set.
 135  
      */
 136  
     public void setPollingFrequency(long pollingFrequency)
 137  
     {
 138  30
         this.pollingFrequency = pollingFrequency;
 139  30
     }
 140  
 
 141  
     /**
 142  
      * Getter for property 'connectionFactoryClass'.
 143  
      *
 144  
      * @return Value for property 'connectionFactoryClass'.
 145  
      */
 146  
     public String getConnectionFactoryClass()
 147  
     {
 148  10
         return connectionFactoryClass;
 149  
     }
 150  
 
 151  
     /**
 152  
      * Setter for property 'connectionFactoryClass'. Should be an instance of
 153  
      * {@link FtpConnectionFactory}.
 154  
      *
 155  
      * @param connectionFactoryClass Value to set for property 'connectionFactoryClass'.
 156  
      */
 157  
     public void setConnectionFactoryClass(final String connectionFactoryClass)
 158  
     {
 159  4
         this.connectionFactoryClass = connectionFactoryClass;
 160  4
     }
 161  
 
 162  
     public FTPClient getFtp(EndpointURI uri) throws Exception
 163  
     {
 164  34
         if (logger.isDebugEnabled())
 165  
         {
 166  0
             logger.debug(">>> retrieving client for " + uri);
 167  
         }
 168  34
         return (FTPClient) getFtpPool(uri).borrowObject();
 169  
     }
 170  
 
 171  
     public void releaseFtp(EndpointURI uri, FTPClient client) throws Exception
 172  
     {
 173  22
         if (logger.isDebugEnabled())
 174  
         {
 175  0
             logger.debug("<<< releasing client for " + uri);
 176  
         }
 177  22
         if (dispatcherFactory.isCreateDispatcherPerRequest())
 178  
         {
 179  0
             destroyFtp(uri, client);
 180  
         }
 181  
         else
 182  
         {
 183  22
             getFtpPool(uri).returnObject(client);
 184  
         }
 185  22
     }
 186  
 
 187  
     public void destroyFtp(EndpointURI uri, FTPClient client) throws Exception
 188  
     {
 189  0
         if (logger.isDebugEnabled())
 190  
         {
 191  0
             logger.debug("<<< destroying client for " + uri);
 192  
         }
 193  
         try
 194  
         {
 195  0
             getFtpPool(uri).invalidateObject(client);
 196  
         }
 197  0
         catch (Exception e)
 198  
         {
 199  
             // no way to test if pool is closed except try to access it
 200  0
             logger.debug(e.getMessage());
 201  0
         }
 202  0
     }
 203  
 
 204  
     protected synchronized ObjectPool getFtpPool(EndpointURI uri)
 205  
     {
 206  58
         if (logger.isDebugEnabled())
 207  
         {
 208  0
             logger.debug("=== get pool for " + uri);
 209  
         }
 210  58
         String key = uri.getUser() + ":" + uri.getPassword() + "@" + uri.getHost() + ":" + uri.getPort();
 211  58
         ObjectPool pool = (ObjectPool) pools.get(key);
 212  58
         if (pool == null)
 213  
         {
 214  
             try
 215  
             {
 216  8
                 FtpConnectionFactory connectionFactory =
 217  
                         (FtpConnectionFactory) ClassUtils.instanciateClass(getConnectionFactoryClass(),
 218  
                                                                             new Object[] {uri}, getClass());
 219  8
                 pool = new GenericObjectPool(connectionFactory);
 220  8
                 ((GenericObjectPool) pool).setTestOnBorrow(this.validateConnections);
 221  8
                 pools.put(key, pool);
 222  
             }
 223  0
             catch (Exception ex)
 224  
             {
 225  0
                 throw new MuleRuntimeException(
 226  
                         MessageFactory.createStaticMessage("Hmm, couldn't instanciate FTP connection factory."), ex);
 227  8
             }
 228  
         }
 229  58
         return pool;
 230  
     }
 231  
 
 232  
 
 233  
     protected void doInitialise() throws InitialisationException
 234  
     {
 235  
         try
 236  
         {
 237  32
             Class objectFactoryClass = ClassUtils.loadClass(this.connectionFactoryClass, getClass());
 238  32
             if (!FtpConnectionFactory.class.isAssignableFrom(objectFactoryClass))
 239  
             {
 240  0
                 throw new InitialisationException(MessageFactory.createStaticMessage(
 241  
                         "FTP connectionFactoryClass is not an instance of org.mule.transport.ftp.FtpConnectionFactory"),
 242  
                         this);
 243  
             }
 244  
         }
 245  0
         catch (ClassNotFoundException e)
 246  
         {
 247  0
             throw new InitialisationException(e, this);
 248  32
         }
 249  32
     }
 250  
 
 251  
     protected void doDispose()
 252  
     {
 253  
         // template method
 254  54
     }
 255  
 
 256  
     protected void doConnect() throws Exception
 257  
     {
 258  
         // template method
 259  32
     }
 260  
 
 261  
     protected void doDisconnect() throws Exception
 262  
     {
 263  
         // template method
 264  32
     }
 265  
 
 266  
     protected void doStart() throws MuleException
 267  
     {
 268  
         // template method
 269  32
     }
 270  
 
 271  
     protected void doStop() throws MuleException
 272  
     {
 273  32
         if (logger.isDebugEnabled())
 274  
         {
 275  0
             logger.debug("!!! stopping all pools");
 276  
         }
 277  
         try
 278  
         {
 279  32
             for (Iterator it = pools.values().iterator(); it.hasNext();)
 280  
             {
 281  8
                 ObjectPool pool = (ObjectPool)it.next();
 282  8
                 pool.close();
 283  8
             }
 284  
         }
 285  0
         catch (Exception e)
 286  
         {
 287  0
             throw new ConnectorException(CoreMessages.failedToStop("FTP Connector"), this, e);
 288  32
         }
 289  32
     }
 290  
 
 291  
     /**
 292  
      * @return Returns the outputPattern.
 293  
      */
 294  
     public String getOutputPattern()
 295  
     {
 296  18
         return outputPattern;
 297  
     }
 298  
 
 299  
     /**
 300  
      * @param outputPattern The outputPattern to set.
 301  
      */
 302  
     public void setOutputPattern(String outputPattern)
 303  
     {
 304  2
         this.outputPattern = outputPattern;
 305  2
     }
 306  
 
 307  
     /**
 308  
      * @return Returns the filenameParser.
 309  
      */
 310  
     public FilenameParser getFilenameParser()
 311  
     {
 312  12
         return filenameParser;
 313  
     }
 314  
 
 315  
     /**
 316  
      * @param filenameParser The filenameParser to set.
 317  
      */
 318  
     public void setFilenameParser(FilenameParser filenameParser)
 319  
     {
 320  2
         this.filenameParser = filenameParser;
 321  2
     }
 322  
 
 323  
     /**
 324  
      * Getter for FTP passive mode.
 325  
      * 
 326  
      * @return true if using FTP passive mode
 327  
      */
 328  
     public boolean isPassive()
 329  
     {
 330  25
         return passive;
 331  
     }
 332  
 
 333  
     /**
 334  
      * Setter for FTP passive mode.
 335  
      * 
 336  
      * @param passive passive mode flag
 337  
      */
 338  
     public void setPassive(final boolean passive)
 339  
     {
 340  2
         this.passive = passive;
 341  2
     }
 342  
 
 343  
     /**
 344  
      * Passive mode is OFF by default. The value is taken from the connector
 345  
      * settings. In case there are any overriding properties set on the endpoint,
 346  
      * those will be used.
 347  
      * 
 348  
      * @see #setPassive(boolean)
 349  
      */
 350  
     public void enterActiveOrPassiveMode(FTPClient client, ImmutableEndpoint endpoint)
 351  
     {
 352  
         // well, no endpoint URI here, as we have to use the most common denominator
 353  
         // in API :(
 354  23
         final String passiveString = (String)endpoint.getProperty(FtpConnector.PROPERTY_PASSIVE_MODE);
 355  23
         if (passiveString == null)
 356  
         {
 357  
             // try the connector properties then
 358  23
             if (isPassive())
 359  
             {
 360  23
                 if (logger.isTraceEnabled())
 361  
                 {
 362  0
                     logger.trace("Entering FTP passive mode");
 363  
                 }
 364  23
                 client.enterLocalPassiveMode();
 365  
             }
 366  
             else
 367  
             {
 368  0
                 if (logger.isTraceEnabled())
 369  
                 {
 370  0
                     logger.trace("Entering FTP active mode");
 371  
                 }
 372  0
                 client.enterLocalActiveMode();
 373  
             }
 374  
         }
 375  
         else
 376  
         {
 377  
             // override with endpoint's definition
 378  0
             final boolean passiveMode = Boolean.valueOf(passiveString).booleanValue();
 379  0
             if (passiveMode)
 380  
             {
 381  0
                 if (logger.isTraceEnabled())
 382  
                 {
 383  0
                     logger.trace("Entering FTP passive mode (endpoint override)");
 384  
                 }
 385  0
                 client.enterLocalPassiveMode();
 386  
             }
 387  
             else
 388  
             {
 389  0
                 if (logger.isTraceEnabled())
 390  
                 {
 391  0
                     logger.trace("Entering FTP active mode (endpoint override)");
 392  
                 }
 393  0
                 client.enterLocalActiveMode();
 394  
             }
 395  
         }
 396  23
     }
 397  
 
 398  
     /**
 399  
      * Whether to test FTP connection on each take from pool.
 400  
      */
 401  
     public boolean isValidateConnections()
 402  
     {
 403  2
         return validateConnections;
 404  
     }
 405  
 
 406  
     /**
 407  
      * Whether to test FTP connection on each take from pool. This takes care of a
 408  
      * failed (or restarted) FTP server at the expense of an additional NOOP command
 409  
      * packet being sent, but increases overall availability. <p/> Disable to gain
 410  
      * slight performance gain or if you are absolutely sure of the FTP server
 411  
      * availability. <p/> The default value is <code>true</code>
 412  
      */
 413  
     public void setValidateConnections(final boolean validateConnections)
 414  
     {
 415  6
         this.validateConnections = validateConnections;
 416  6
     }
 417  
 
 418  
     /**
 419  
      * Getter for FTP transfer type.
 420  
      * 
 421  
      * @return true if using FTP binary type
 422  
      */
 423  
     public boolean isBinary()
 424  
     {
 425  25
         return binary;
 426  
     }
 427  
 
 428  
     /**
 429  
      * Setter for FTP transfer type.
 430  
      * 
 431  
      * @param binary binary type flag
 432  
      */
 433  
     public void setBinary(final boolean binary)
 434  
     {
 435  2
         this.binary = binary;
 436  2
     }
 437  
 
 438  
     /**
 439  
      * Transfer type is BINARY by default. The value is taken from the connector
 440  
      * settings. In case there are any overriding properties set on the endpoint,
 441  
      * those will be used. <p/> The alternative type is ASCII. <p/>
 442  
      * 
 443  
      * @see #setBinary(boolean)
 444  
      */
 445  
     public void setupFileType(FTPClient client, ImmutableEndpoint endpoint) throws Exception
 446  
     {
 447  
         int type;
 448  
 
 449  
         // well, no endpoint URI here, as we have to use the most common denominator
 450  
         // in API :(
 451  23
         final String binaryTransferString = (String)endpoint.getProperty(FtpConnector.PROPERTY_BINARY_TRANSFER);
 452  23
         if (binaryTransferString == null)
 453  
         {
 454  
             // try the connector properties then
 455  23
             if (isBinary())
 456  
             {
 457  23
                 if (logger.isTraceEnabled())
 458  
                 {
 459  0
                     logger.trace("Using FTP BINARY type");
 460  
                 }
 461  23
                 type = org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE;
 462  
             }
 463  
             else
 464  
             {
 465  0
                 if (logger.isTraceEnabled())
 466  
                 {
 467  0
                     logger.trace("Using FTP ASCII type");
 468  
                 }
 469  0
                 type = org.apache.commons.net.ftp.FTP.ASCII_FILE_TYPE;
 470  
             }
 471  
         }
 472  
         else
 473  
         {
 474  
             // override with endpoint's definition
 475  0
             final boolean binaryTransfer = Boolean.valueOf(binaryTransferString).booleanValue();
 476  0
             if (binaryTransfer)
 477  
             {
 478  0
                 if (logger.isTraceEnabled())
 479  
                 {
 480  0
                     logger.trace("Using FTP BINARY type (endpoint override)");
 481  
                 }
 482  0
                 type = org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE;
 483  
             }
 484  
             else
 485  
             {
 486  0
                 if (logger.isTraceEnabled())
 487  
                 {
 488  0
                     logger.trace("Using FTP ASCII type (endpoint override)");
 489  
                 }
 490  0
                 type = org.apache.commons.net.ftp.FTP.ASCII_FILE_TYPE;
 491  
             }
 492  
         }
 493  
 
 494  23
         client.setFileType(type);
 495  22
     }
 496  
 
 497  
     /**
 498  
      * Well get the output stream (if any) for this type of transport. Typically this
 499  
      * will be called only when Streaming is being used on an outbound endpoint
 500  
      *
 501  
      * @param endpoint the endpoint that releates to this Dispatcher
 502  
      * @param message the current message being processed
 503  
      * @return the output stream to use for this request or null if the transport
 504  
      *         does not support streaming
 505  
      * @throws org.mule.api.MuleException
 506  
      */
 507  
     public OutputStream getOutputStream(ImmutableEndpoint endpoint, MuleMessage message)
 508  
         throws MuleException
 509  
     {
 510  
         try
 511  
         {
 512  8
             final EndpointURI uri = endpoint.getEndpointURI();
 513  8
             String filename = getFilename(endpoint, message);
 514  
 
 515  8
             final FTPClient client = this.createFtpClient(endpoint);
 516  
             try
 517  
             {
 518  8
                 OutputStream out = client.storeFileStream(filename);
 519  8
                 return new CallbackOutputStream(out,
 520  
                         new CallbackOutputStream.Callback()
 521  
                         {
 522  8
                             public void onClose() throws Exception
 523  
                             {
 524  
                                 try
 525  
                                 {
 526  8
                                     if (!client.completePendingCommand())
 527  
                                     {
 528  0
                                         client.logout();
 529  0
                                         client.disconnect();
 530  0
                                         throw new IOException("FTP Stream failed to complete pending request");
 531  
                                     }
 532  
                                 }
 533  
                                 finally
 534  
                                 {
 535  8
                                     releaseFtp(uri, client);
 536  8
                                 }
 537  8
                             }
 538  
                         });
 539  
             }
 540  0
             catch (Exception e)
 541  
             {
 542  0
                 logger.debug("Error getting output stream: ", e);
 543  0
                 releaseFtp(uri, client);
 544  0
                 throw e;
 545  
             }
 546  
         }
 547  0
         catch (Exception e)
 548  
         {
 549  0
             throw new DispatchException(CoreMessages.streamingFailedNoStream(), message, endpoint, e);
 550  
         }
 551  
     }
 552  
 
 553  
     private String getFilename(ImmutableEndpoint endpoint, MuleMessage message) throws IOException
 554  
     {
 555  8
         String filename = (String) message.getProperty(FtpConnector.PROPERTY_FILENAME);
 556  8
         String outPattern = (String) endpoint.getProperty(FtpConnector.PROPERTY_OUTPUT_PATTERN);
 557  8
         if (outPattern == null)
 558  
         {
 559  8
             outPattern = message.getStringProperty(FtpConnector.PROPERTY_OUTPUT_PATTERN, getOutputPattern());
 560  
         }
 561  8
         if (outPattern != null || filename == null)
 562  
         {
 563  8
             filename = generateFilename(message, outPattern);
 564  
         }
 565  8
         if (filename == null)
 566  
         {
 567  0
             throw new IOException("Filename is null");
 568  
         }
 569  8
         return filename;
 570  
     }
 571  
     
 572  
     private String generateFilename(MuleMessage message, String pattern)
 573  
     {
 574  8
         if (pattern == null)
 575  
         {
 576  8
             pattern = getOutputPattern();
 577  
         }
 578  8
         return getFilenameParser().getFilename(message, pattern);
 579  
     }
 580  
 
 581  
     /**
 582  
      * Creates a new FTPClient that logs in and changes the working directory using the data
 583  
      * provided in <code>endpoint</code>.
 584  
      */
 585  
     protected FTPClient createFtpClient(ImmutableEndpoint endpoint) throws Exception
 586  
     {
 587  26
         EndpointURI uri = endpoint.getEndpointURI();
 588  26
         FTPClient client = this.getFtp(uri);
 589  
 
 590  23
         this.enterActiveOrPassiveMode(client, endpoint);
 591  23
         this.setupFileType(client, endpoint);
 592  
 
 593  22
         String path = uri.getPath();
 594  
         // MULE-2400: if the path begins with '~' we must strip the first '/' to make things
 595  
         // work with FTPClient
 596  22
         if ((path.length() >= 2) && (path.charAt(1) == '~'))
 597  
         {
 598  0
             path = path.substring(1);
 599  
         }
 600  
         
 601  22
         if (!client.changeWorkingDirectory(path))
 602  
         {
 603  0
             throw new IOException(MessageFormat.format("Failed to change working directory to {0}. Ftp error: {1}",
 604  
                                                        new Object[] {path, new Integer(client.getReplyCode())}));
 605  
         }
 606  22
         return client;
 607  
     }
 608  
 
 609  
     /**
 610  
      * Override this method to do extra checking on the file.
 611  
      */
 612  
     protected boolean validateFile(FTPFile file)
 613  
     {
 614  8
         return true;
 615  
     }
 616  
 }