001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.camel.component.file.remote;
018    
019    import java.io.ByteArrayOutputStream;
020    import java.io.IOException;
021    import java.util.concurrent.ScheduledExecutorService;
022    
023    import org.apache.camel.Processor;
024    import org.apache.camel.component.file.FileComponent;
025    import org.apache.camel.util.ObjectHelper;
026    import org.apache.commons.net.ftp.FTPClient;
027    import org.apache.commons.net.ftp.FTPFile;
028    
029    public class FtpConsumer extends RemoteFileConsumer<RemoteFileExchange> {
030    
031        private FtpEndpoint endpoint;
032        private FTPClient client;
033        private boolean loggedIn;
034    
035        public FtpConsumer(FtpEndpoint endpoint, Processor processor, FTPClient client) {
036            super(endpoint, processor);
037            this.endpoint = endpoint;
038            this.client = client;
039        }
040    
041        public FtpConsumer(FtpEndpoint endpoint, Processor processor, FTPClient client,
042                           ScheduledExecutorService executor) {
043            super(endpoint, processor, executor);
044            this.endpoint = endpoint;
045            this.client = client;
046        }
047    
048        protected void doStart() throws Exception {
049            log.info("Starting");
050            super.doStart();
051        }
052    
053        protected void doStop() throws Exception {
054            log.info("Stopping");
055            // disconnect when stopping
056            try {
057                disconnect();
058            } catch (Exception e) {
059                // ignore just log a warning
060                String message = "Could not disconnect from " + remoteServer()
061                        + ". Reason: " + client.getReplyString() + ". Code: " + client.getReplyCode();
062                log.warn(message);
063            }
064            super.doStop();
065        }
066    
067        protected void connectIfNecessary() throws IOException {
068            if (!client.isConnected() || !loggedIn) {
069                if (log.isDebugEnabled()) {
070                    log.debug("Not connected/logged in, connecting to " + remoteServer());
071                }
072    
073                try {
074                    loggedIn = FtpUtils.connect(client, endpoint.getConfiguration());
075                } catch (java.net.ConnectException ce) {
076                    // Wait a bit to make sure we have something to connect to and
077                    // try once more. 
078                    try {
079                        Thread.sleep(3000);
080                    } catch (InterruptedException ie) {
081                        // ignore.
082                    }
083                    loggedIn = FtpUtils.connect(client, endpoint.getConfiguration());
084                }
085                if (!loggedIn) {
086                    return;
087                }
088            }
089            
090            log.info("Connected and logged in to " + remoteServer());
091        }
092    
093        protected void disconnect() throws IOException {
094            loggedIn = false;
095            log.debug("Disconnecting from " + remoteServer());
096            FtpUtils.disconnect(client);
097        }
098    
099        protected void poll() throws Exception {
100            if (log.isTraceEnabled()) {
101                log.trace("Polling " + endpoint.getConfiguration());
102            }
103    
104            try {
105                connectIfNecessary();
106    
107                if (!loggedIn) {
108                    String message = "Could not connect/login to " + endpoint.getConfiguration();
109                    log.warn(message);
110                    throw new FtpOperationFailedException(client.getReplyCode(), client.getReplyString(), message);
111                }
112    
113                final String fileName = endpoint.getConfiguration().getFile();
114                if (endpoint.getConfiguration().isDirectory()) {
115                    pollDirectory(fileName);
116                } else {
117                    int index = fileName.lastIndexOf('/');
118                    if (index > -1) {
119                        // cd to the folder of the filename
120                        client.changeWorkingDirectory(fileName.substring(0, index));
121                    }
122                    // list the files in the fold and poll the first file
123                    final FTPFile[] files = client.listFiles(fileName.substring(index + 1));
124                    if (files != null && files.length > 0) {
125                        pollFile(files[0]);
126                    }
127                }
128    
129                lastPollTime = System.currentTimeMillis();
130    
131            } catch (Exception e) {
132                loggedIn = false;
133                if (isStopping() || isStopped()) {
134                    // if we are stopping then ignore any exception during a poll
135                    log.warn("Consumer is stopping. Ignoring caught exception: "
136                             + e.getClass().getCanonicalName() + " message: " + e.getMessage());
137                } else {
138                    log.warn("Exception occurred during polling: "
139                             + e.getClass().getCanonicalName() + " message: " + e.getMessage());
140                    disconnect();
141                    // Rethrow to signify that we didn't poll
142                    throw e;
143                }
144            }
145        }
146    
147        protected void pollDirectory(String dir) throws Exception {
148            if (log.isTraceEnabled()) {
149                log.trace("Polling directory: " + dir);
150            }
151            String currentDir = client.printWorkingDirectory();
152    
153            client.changeWorkingDirectory(dir);
154            for (FTPFile ftpFile : client.listFiles()) {
155                if (ftpFile.isFile()) {
156                    pollFile(ftpFile);
157                } else if (ftpFile.isDirectory()) {
158                    if (isRecursive()) {
159                        pollDirectory(getFullFileName(ftpFile));
160                    }
161                } else {
162                    log.debug("Unsupported type of FTPFile: " + ftpFile + " (not a file or directory). It is skipped.");
163                }
164            }
165    
166            // change back to original current dir
167            client.changeWorkingDirectory(currentDir);
168        }
169    
170        protected String getFullFileName(FTPFile ftpFile) throws IOException {
171            return client.printWorkingDirectory() + "/" + ftpFile.getName();
172        }
173    
174        private void pollFile(FTPFile ftpFile) throws Exception {
175            if (ftpFile == null) {
176                return;
177            }
178    
179            if (log.isTraceEnabled()) {
180                log.trace("Polling file: " + ftpFile);
181            }
182    
183            // if using last polltime for timestamp matcing (to be removed in Camel 2.0)
184            boolean timestampMatched = true;
185            if (isTimestamp()) {
186                // TODO do we need to adjust the TZ? can we?
187                long ts = ftpFile.getTimestamp().getTimeInMillis();
188                timestampMatched = ts > lastPollTime;
189                if (log.isTraceEnabled()) {
190                    log.trace("The file is to old + " + ftpFile + ". lastPollTime=" + lastPollTime + " > fileTimestamp=" + ts);
191                }
192            }
193    
194            if (timestampMatched && isMatched(ftpFile)) {
195                String fullFileName = getFullFileName(ftpFile);
196    
197                // is we use exclusive read then acquire the exclusive read (waiting until we got it)
198                if (exclusiveReadLock) {
199                    acquireExclusiveReadLock(client, ftpFile);
200                }
201    
202                // retrieve the file
203                final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
204                try {
205                    client.retrieveFile(ftpFile.getName(), byteArrayOutputStream);
206                    if (log.isDebugEnabled()) {
207                        log.debug("Retrieved file: " + ftpFile.getName() + " from: " + remoteServer());
208                    }
209                } finally {
210                    // close stream to avoid leaking FTP connections
211                    ObjectHelper.close(byteArrayOutputStream, "retrieve: " + ftpFile.getName(), log);
212                }
213    
214                RemoteFileExchange exchange = endpoint.createExchange(fullFileName, ftpFile.getName(),
215                        ftpFile.getSize(), byteArrayOutputStream);
216    
217                if (isSetNames()) {
218                    // set the filename in the special header filename marker to the ftp filename
219                    String ftpBasePath = endpoint.getConfiguration().getFile();
220                    String relativePath = fullFileName.substring(ftpBasePath.length() + 1);
221                    relativePath = relativePath.replaceFirst("/", "");
222    
223                    if (log.isDebugEnabled()) {
224                        log.debug("Setting exchange filename to " + relativePath);
225                    }
226                    exchange.getIn().setHeader(FileComponent.HEADER_FILE_NAME, relativePath);
227                }
228    
229    
230                // all success so lets process it
231                getProcessor().process(exchange);
232    
233                if (exchange.isFailed()) {
234                    if (log.isDebugEnabled()) {
235                        log.debug("Processing of exchange failed, so cannot do FTP post command such as move or delete: " + exchange);
236                    }
237                } else {
238                    // after processing then do post command such as delete or move
239                    if (deleteFile) {
240                        // delete file after consuming
241                        if (log.isDebugEnabled()) {
242                            log.debug("Deleting file: " + ftpFile.getName() + " from: " + remoteServer());
243                        }
244                        boolean deleted = client.deleteFile(ftpFile.getName());
245                        if (!deleted) {
246                            String message = "Can not delete file: " + ftpFile.getName() + " from: " + remoteServer();
247                            throw new FtpOperationFailedException(client.getReplyCode(), client.getReplyString(), message);
248                        }
249                    } else if (isMoveFile()) {
250                        String fromName = ftpFile.getName();
251                        String toName = getMoveFileName(fromName, exchange);
252                        if (log.isDebugEnabled()) {
253                            log.debug("Moving file: " + fromName + " to: " + toName);
254                        }
255    
256                        // delete any existing file
257                        boolean deleted = client.deleteFile(toName);
258                        if (!deleted) {
259                            // if we could not delete any existing file then maybe the folder is missing
260                            // build folder if needed
261                            int lastPathIndex = toName.lastIndexOf('/');
262                            if (lastPathIndex != -1) {
263                                String directory = toName.substring(0, lastPathIndex);
264                                if (!FtpUtils.buildDirectory(client, directory)) {
265                                    log.warn("Can not build directory: " + directory + " (maybe because of denied permissions)");
266                                }
267                            }
268                        }
269    
270                        // try to rename
271                        boolean success = client.rename(fromName, toName);
272                        if (!success) {
273                            String message = "Can not move file: " + fromName + " to: " + toName;
274                            throw new FtpOperationFailedException(client.getReplyCode(), client.getReplyString(), message);
275                        }
276                    }
277                }
278    
279            }
280        }
281    
282        protected void acquireExclusiveReadLock(FTPClient client, FTPFile ftpFile) throws IOException {
283            if (log.isTraceEnabled()) {
284                log.trace("Waiting for exclusive read lock to file: " + ftpFile);
285            }
286    
287            // the trick is to try to rename the file, if we can rename then we have exclusive read
288            // since its a remote file we can not use java.nio to get a RW lock
289            String originalName = ftpFile.getName();
290            String newName = originalName + ".camelExclusiveReadLock";
291            boolean exclusive = false;
292            while (!exclusive) {
293                exclusive = client.rename(originalName, newName);
294                if (exclusive) {
295                    if (log.isDebugEnabled()) {
296                        log.debug("Acquired exclusive read lock to file: " + originalName);
297                    }
298                    // rename it back so we can read it
299                    client.rename(newName, originalName);
300                } else {
301                    log.trace("Exclusive read lock not granted. Sleeping for 1000 millis.");
302                    try {
303                        Thread.sleep(1000);
304                    } catch (InterruptedException e) {
305                        // ignore
306                    }
307                }
308            }
309        }
310    
311        protected String getFileName(Object file) {
312            FTPFile ftpFile = (FTPFile) file;
313            return ftpFile.getName();
314        }
315    
316    }