View Javadoc

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  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      private FilenameParser filenameParser = new SimpleFilenameParser();
76  
77      private boolean passive = true;
78  
79      private boolean binary = true;
80  
81      /**
82       * Whether to test FTP connection on each take from pool.
83       */
84      private boolean validateConnections = true;
85  
86      private Map pools = new HashMap();
87  
88      private String connectionFactoryClass = DEFAULT_FTP_CONNECTION_FACTORY_CLASS;
89  
90      public String getProtocol()
91      {
92          return FTP;
93      }
94  
95      public MessageReceiver createReceiver(Service service, InboundEndpoint endpoint) throws Exception
96      {
97          List args = getReceiverArguments(endpoint.getProperties());
98          return serviceDescriptor.createMessageReceiver(this, service, endpoint, args.toArray());
99      }
100 
101     protected List getReceiverArguments(Map endpointProperties)
102     {
103         List args = new ArrayList();
104         
105         long polling = getPollingFrequency();
106         if (endpointProperties != null)
107         {
108             // Override properties on the endpoint for the specific endpoint
109             String tempPolling = (String) endpointProperties.get(PROPERTY_POLLING_FREQUENCY);
110             if (tempPolling != null)
111             {
112                 polling = Long.parseLong(tempPolling);
113             }
114         }
115         if (polling <= 0)
116         {
117             polling = DEFAULT_POLLING_FREQUENCY;
118         }
119         logger.debug("set polling frequency to " + polling);
120         args.add(new Long(polling));
121         
122         return args;
123     }
124     
125     /**
126      * @return Returns the pollingFrequency.
127      */
128     public long getPollingFrequency()
129     {
130         return pollingFrequency;
131     }
132 
133     /**
134      * @param pollingFrequency The pollingFrequency to set.
135      */
136     public void setPollingFrequency(long pollingFrequency)
137     {
138         this.pollingFrequency = pollingFrequency;
139     }
140 
141     /**
142      * Getter for property 'connectionFactoryClass'.
143      *
144      * @return Value for property 'connectionFactoryClass'.
145      */
146     public String getConnectionFactoryClass()
147     {
148         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         this.connectionFactoryClass = connectionFactoryClass;
160     }
161 
162     public FTPClient getFtp(EndpointURI uri) throws Exception
163     {
164         if (logger.isDebugEnabled())
165         {
166             logger.debug(">>> retrieving client for " + uri);
167         }
168         return (FTPClient) getFtpPool(uri).borrowObject();
169     }
170 
171     public void releaseFtp(EndpointURI uri, FTPClient client) throws Exception
172     {
173         if (logger.isDebugEnabled())
174         {
175             logger.debug("<<< releasing client for " + uri);
176         }
177         if (dispatcherFactory.isCreateDispatcherPerRequest())
178         {
179             destroyFtp(uri, client);
180         }
181         else
182         {
183             getFtpPool(uri).returnObject(client);
184         }
185     }
186 
187     public void destroyFtp(EndpointURI uri, FTPClient client) throws Exception
188     {
189         if (logger.isDebugEnabled())
190         {
191             logger.debug("<<< destroying client for " + uri);
192         }
193         try
194         {
195             getFtpPool(uri).invalidateObject(client);
196         }
197         catch (Exception e)
198         {
199             // no way to test if pool is closed except try to access it
200             logger.debug(e.getMessage());
201         }
202     }
203 
204     protected synchronized ObjectPool getFtpPool(EndpointURI uri)
205     {
206         if (logger.isDebugEnabled())
207         {
208             logger.debug("=== get pool for " + uri);
209         }
210         String key = uri.getUser() + ":" + uri.getPassword() + "@" + uri.getHost() + ":" + uri.getPort();
211         ObjectPool pool = (ObjectPool) pools.get(key);
212         if (pool == null)
213         {
214             try
215             {
216                 FtpConnectionFactory connectionFactory =
217                         (FtpConnectionFactory) ClassUtils.instanciateClass(getConnectionFactoryClass(),
218                                                                             new Object[] {uri}, getClass());
219                 pool = new GenericObjectPool(connectionFactory);
220                 ((GenericObjectPool) pool).setTestOnBorrow(this.validateConnections);
221                 pools.put(key, pool);
222             }
223             catch (Exception ex)
224             {
225                 throw new MuleRuntimeException(
226                         MessageFactory.createStaticMessage("Hmm, couldn't instanciate FTP connection factory."), ex);
227             }
228         }
229         return pool;
230     }
231 
232 
233     protected void doInitialise() throws InitialisationException
234     {
235         try
236         {
237             Class objectFactoryClass = ClassUtils.loadClass(this.connectionFactoryClass, getClass());
238             if (!FtpConnectionFactory.class.isAssignableFrom(objectFactoryClass))
239             {
240                 throw new InitialisationException(MessageFactory.createStaticMessage(
241                         "FTP connectionFactoryClass is not an instance of org.mule.transport.ftp.FtpConnectionFactory"),
242                         this);
243             }
244         }
245         catch (ClassNotFoundException e)
246         {
247             throw new InitialisationException(e, this);
248         }
249     }
250 
251     protected void doDispose()
252     {
253         // template method
254     }
255 
256     protected void doConnect() throws Exception
257     {
258         // template method
259     }
260 
261     protected void doDisconnect() throws Exception
262     {
263         // template method
264     }
265 
266     protected void doStart() throws MuleException
267     {
268         // template method
269     }
270 
271     protected void doStop() throws MuleException
272     {
273         if (logger.isDebugEnabled())
274         {
275             logger.debug("!!! stopping all pools");
276         }
277         try
278         {
279             for (Iterator it = pools.values().iterator(); it.hasNext();)
280             {
281                 ObjectPool pool = (ObjectPool)it.next();
282                 pool.close();
283             }
284         }
285         catch (Exception e)
286         {
287             throw new ConnectorException(CoreMessages.failedToStop("FTP Connector"), this, e);
288         }
289     }
290 
291     /**
292      * @return Returns the outputPattern.
293      */
294     public String getOutputPattern()
295     {
296         return outputPattern;
297     }
298 
299     /**
300      * @param outputPattern The outputPattern to set.
301      */
302     public void setOutputPattern(String outputPattern)
303     {
304         this.outputPattern = outputPattern;
305     }
306 
307     /**
308      * @return Returns the filenameParser.
309      */
310     public FilenameParser getFilenameParser()
311     {
312         return filenameParser;
313     }
314 
315     /**
316      * @param filenameParser The filenameParser to set.
317      */
318     public void setFilenameParser(FilenameParser filenameParser)
319     {
320         this.filenameParser = filenameParser;
321     }
322 
323     /**
324      * Getter for FTP passive mode.
325      * 
326      * @return true if using FTP passive mode
327      */
328     public boolean isPassive()
329     {
330         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         this.passive = passive;
341     }
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         final String passiveString = (String)endpoint.getProperty(FtpConnector.PROPERTY_PASSIVE_MODE);
355         if (passiveString == null)
356         {
357             // try the connector properties then
358             if (isPassive())
359             {
360                 if (logger.isTraceEnabled())
361                 {
362                     logger.trace("Entering FTP passive mode");
363                 }
364                 client.enterLocalPassiveMode();
365             }
366             else
367             {
368                 if (logger.isTraceEnabled())
369                 {
370                     logger.trace("Entering FTP active mode");
371                 }
372                 client.enterLocalActiveMode();
373             }
374         }
375         else
376         {
377             // override with endpoint's definition
378             final boolean passiveMode = Boolean.valueOf(passiveString).booleanValue();
379             if (passiveMode)
380             {
381                 if (logger.isTraceEnabled())
382                 {
383                     logger.trace("Entering FTP passive mode (endpoint override)");
384                 }
385                 client.enterLocalPassiveMode();
386             }
387             else
388             {
389                 if (logger.isTraceEnabled())
390                 {
391                     logger.trace("Entering FTP active mode (endpoint override)");
392                 }
393                 client.enterLocalActiveMode();
394             }
395         }
396     }
397 
398     /**
399      * Whether to test FTP connection on each take from pool.
400      */
401     public boolean isValidateConnections()
402     {
403         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         this.validateConnections = validateConnections;
416     }
417 
418     /**
419      * Getter for FTP transfer type.
420      * 
421      * @return true if using FTP binary type
422      */
423     public boolean isBinary()
424     {
425         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         this.binary = binary;
436     }
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         final String binaryTransferString = (String)endpoint.getProperty(FtpConnector.PROPERTY_BINARY_TRANSFER);
452         if (binaryTransferString == null)
453         {
454             // try the connector properties then
455             if (isBinary())
456             {
457                 if (logger.isTraceEnabled())
458                 {
459                     logger.trace("Using FTP BINARY type");
460                 }
461                 type = org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE;
462             }
463             else
464             {
465                 if (logger.isTraceEnabled())
466                 {
467                     logger.trace("Using FTP ASCII type");
468                 }
469                 type = org.apache.commons.net.ftp.FTP.ASCII_FILE_TYPE;
470             }
471         }
472         else
473         {
474             // override with endpoint's definition
475             final boolean binaryTransfer = Boolean.valueOf(binaryTransferString).booleanValue();
476             if (binaryTransfer)
477             {
478                 if (logger.isTraceEnabled())
479                 {
480                     logger.trace("Using FTP BINARY type (endpoint override)");
481                 }
482                 type = org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE;
483             }
484             else
485             {
486                 if (logger.isTraceEnabled())
487                 {
488                     logger.trace("Using FTP ASCII type (endpoint override)");
489                 }
490                 type = org.apache.commons.net.ftp.FTP.ASCII_FILE_TYPE;
491             }
492         }
493 
494         client.setFileType(type);
495     }
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             final EndpointURI uri = endpoint.getEndpointURI();
513             String filename = getFilename(endpoint, message);
514 
515             final FTPClient client = this.createFtpClient(endpoint);
516             try
517             {
518                 OutputStream out = client.storeFileStream(filename);
519                 return new CallbackOutputStream(out,
520                         new CallbackOutputStream.Callback()
521                         {
522                             public void onClose() throws Exception
523                             {
524                                 try
525                                 {
526                                     if (!client.completePendingCommand())
527                                     {
528                                         client.logout();
529                                         client.disconnect();
530                                         throw new IOException("FTP Stream failed to complete pending request");
531                                     }
532                                 }
533                                 finally
534                                 {
535                                     releaseFtp(uri, client);
536                                 }
537                             }
538                         });
539             }
540             catch (Exception e)
541             {
542                 logger.debug("Error getting output stream: ", e);
543                 releaseFtp(uri, client);
544                 throw e;
545             }
546         }
547         catch (Exception e)
548         {
549             throw new DispatchException(CoreMessages.streamingFailedNoStream(), message, endpoint, e);
550         }
551     }
552 
553     private String getFilename(ImmutableEndpoint endpoint, MuleMessage message) throws IOException
554     {
555         String filename = (String) message.getProperty(FtpConnector.PROPERTY_FILENAME);
556         String outPattern = (String) endpoint.getProperty(FtpConnector.PROPERTY_OUTPUT_PATTERN);
557         if (outPattern == null)
558         {
559             outPattern = message.getStringProperty(FtpConnector.PROPERTY_OUTPUT_PATTERN, getOutputPattern());
560         }
561         if (outPattern != null || filename == null)
562         {
563             filename = generateFilename(message, outPattern);
564         }
565         if (filename == null)
566         {
567             throw new IOException("Filename is null");
568         }
569         return filename;
570     }
571     
572     private String generateFilename(MuleMessage message, String pattern)
573     {
574         if (pattern == null)
575         {
576             pattern = getOutputPattern();
577         }
578         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         EndpointURI uri = endpoint.getEndpointURI();
588         FTPClient client = this.getFtp(uri);
589 
590         this.enterActiveOrPassiveMode(client, endpoint);
591         this.setupFileType(client, endpoint);
592 
593         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         if ((path.length() >= 2) && (path.charAt(1) == '~'))
597         {
598             path = path.substring(1);
599         }
600         
601         if (!client.changeWorkingDirectory(path))
602         {
603             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         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         return true;
615     }
616 }