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.Vector;
022    import java.util.concurrent.ScheduledExecutorService;
023    
024    import com.jcraft.jsch.ChannelSftp;
025    import com.jcraft.jsch.JSchException;
026    import com.jcraft.jsch.Session;
027    import com.jcraft.jsch.SftpException;
028    
029    import org.apache.camel.Processor;
030    import org.apache.camel.component.file.FileComponent;
031    import org.apache.camel.util.ObjectHelper;
032    
033    public class SftpConsumer extends RemoteFileConsumer<RemoteFileExchange> {
034        private final SftpEndpoint endpoint;
035    
036        private ChannelSftp channel;
037        private Session session;
038    
039        public SftpConsumer(SftpEndpoint endpoint, Processor processor, Session session) {
040            super(endpoint, processor);
041            this.endpoint = endpoint;
042            this.session = session;
043        }
044    
045        public SftpConsumer(SftpEndpoint endpoint, Processor processor, Session session, ScheduledExecutorService executor) {
046            super(endpoint, processor, executor);
047            this.endpoint = endpoint;
048            this.session = session;
049        }
050    
051        protected void doStart() throws Exception {
052            log.info("Starting");
053            super.doStart();
054        }
055    
056        protected void doStop() throws Exception {
057            log.info("Stopping");
058            // disconnect when stopping
059            try {
060                disconnect();
061            } catch (Exception e) {
062                // ignore just log a warning
063                log.warn("Exception occurred during disconecting from " + remoteServer() + ". "
064                         + e.getClass().getCanonicalName() + " message: " + e.getMessage());
065            }
066            super.doStop();
067        }
068    
069        protected void connectIfNecessary() throws JSchException {
070            if (channel == null || !channel.isConnected()) {
071                if (session == null || !session.isConnected()) {
072                    log.trace("Session isn't connected, trying to recreate and connect.");
073                    session = endpoint.createSession();
074                    session.connect();
075                }
076                log.trace("Channel isn't connected, trying to recreate and connect.");
077                channel = endpoint.createChannelSftp(session);
078                channel.connect();
079                log.info("Connected to " + remoteServer());
080            }
081        }
082    
083        protected void disconnect() throws JSchException {
084            log.debug("Disconnecting from " + remoteServer());
085            if (session != null) {
086                session.disconnect();
087            }
088            if (channel != null) {
089                channel.disconnect();
090            }
091        }
092    
093        protected void poll() throws Exception {
094            if (log.isTraceEnabled()) {
095                log.trace("Polling " + endpoint.getConfiguration());
096            }
097            connectIfNecessary();
098            // If the attempt to connect isn't successful, then the thrown
099            // exception will signify that we couldn't poll
100            try {
101                final String fileName = endpoint.getConfiguration().getFile();
102                if (endpoint.getConfiguration().isDirectory()) {
103                    pollDirectory(fileName);
104                } else {
105                    int index = fileName.lastIndexOf('/');
106                    if (index > -1) {
107                        // cd to the folder of the filename
108                        channel.cd(fileName.substring(0, index));
109                    }
110    
111                    // list the files in the fold and poll the first file
112                    final Vector files = channel.ls(fileName.substring(index + 1));
113                    final ChannelSftp.LsEntry file = (ChannelSftp.LsEntry) files.get(0);
114                    pollFile(file);
115                }
116                lastPollTime = System.currentTimeMillis();
117            } catch (Exception e) {
118                if (isStopping() || isStopped()) {
119                    // if we are stopping then ignore any exception during a poll
120                    log.warn("Consumer is stopping. Ignoring caught exception: "
121                             + e.getClass().getCanonicalName() + " message: " + e.getMessage());
122                } else {
123                    log.warn("Exception occurred during polling: "
124                             + e.getClass().getCanonicalName() + " message: " + e.getMessage());
125                    disconnect();
126                    // Rethrow to signify that we didn't poll
127                    throw e;
128                }
129            }
130        }
131    
132        protected void pollDirectory(String dir) throws Exception {
133            if (log.isTraceEnabled()) {
134                log.trace("Polling directory: " + dir);
135            }
136            String currentDir = channel.pwd();
137    
138            channel.cd(dir);
139            Vector files = channel.ls(".");
140            for (int i = 0; i < files.size(); i++) {
141                ChannelSftp.LsEntry sftpFile = (ChannelSftp.LsEntry)files.get(i);
142                if (sftpFile.getFilename().startsWith(".")) {
143                    // skip
144                } else if (sftpFile.getAttrs().isDir()) {
145                    if (isRecursive()) {
146                        pollDirectory(getFullFileName(sftpFile));
147                    }
148                } else {
149                    pollFile(sftpFile);
150                }
151            }
152    
153            // change back to original current dir
154            channel.cd(currentDir);
155        }
156    
157        protected String getFullFileName(ChannelSftp.LsEntry sftpFile) throws IOException, SftpException {
158            return channel.pwd() + "/" + sftpFile.getFilename();
159        }
160    
161        private void pollFile(ChannelSftp.LsEntry sftpFile) throws Exception {
162            if (log.isTraceEnabled()) {
163                log.trace("Polling file: " + sftpFile);
164            }
165    
166            // if using last polltime for timestamp matcing (to be removed in Camel 2.0)
167            boolean timestampMatched = true;
168            if (isTimestamp()) {
169                // TODO do we need to adjust the TZ? can we?
170                long ts = sftpFile.getAttrs().getMTime() * 1000L;
171                timestampMatched = ts > lastPollTime;
172                if (log.isTraceEnabled()) {
173                    log.trace("The file is to old + " + sftpFile + ". lastPollTime=" + lastPollTime + " > fileTimestamp=" + ts);
174                }
175            }
176    
177            if (timestampMatched && isMatched(sftpFile)) {
178                String fullFileName = getFullFileName(sftpFile);
179    
180                // is we use exclusive read then acquire the exclusive read (waiting until we got it)
181                if (exclusiveReadLock) {
182                    acquireExclusiveReadLock(sftpFile);
183                }
184    
185                // retrieve the file
186                final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
187                try {
188                    channel.get(sftpFile.getFilename(), byteArrayOutputStream);
189                    if (log.isDebugEnabled()) {
190                        log.debug("Retrieved file: " + sftpFile.getFilename() + " from: " + remoteServer());
191                    }
192                } finally {
193                    // close stream to avoid leaking SFTP connections
194                    ObjectHelper.close(byteArrayOutputStream, "retrieve: " + sftpFile.getFilename(), log);
195                }
196    
197                RemoteFileExchange exchange = endpoint.createExchange(getFullFileName(sftpFile),
198                        sftpFile.getFilename(), sftpFile.getAttrs().getSize(), byteArrayOutputStream);
199    
200                if (isSetNames()) {
201                    String ftpBasePath = endpoint.getConfiguration().getFile();
202                    String relativePath = fullFileName.substring(ftpBasePath.length() + 1);
203                    relativePath = relativePath.replaceFirst("/", "");
204    
205                    if (log.isDebugEnabled()) {
206                        log.debug("Setting exchange filename to " + relativePath);
207                    }
208                    exchange.getIn().setHeader(FileComponent.HEADER_FILE_NAME, relativePath);
209                }
210    
211                // all success so lets process it
212                getProcessor().process(exchange);
213    
214                if (exchange.isFailed()) {
215                    if (log.isDebugEnabled()) {
216                        log.debug("Processing of exchange failed, so cannot do FTP post command such as move or delete: " + exchange);
217                    }
218                } else {
219                    // after processing then do post command such as delete or move
220                    if (deleteFile) {
221                        // delete file after consuming
222                        if (log.isDebugEnabled()) {
223                            log.debug("Deleting file: " + sftpFile.getFilename() + " from: " + remoteServer());
224                        }
225                        deleteFile(sftpFile.getFilename());
226                    } else if (isMoveFile()) {
227                        String fromName = sftpFile.getFilename();
228                        String toName = getMoveFileName(fromName, exchange);
229                        if (log.isDebugEnabled()) {
230                            log.debug("Moving file: " + fromName + " to: " + toName);
231                        }
232    
233                        // delete any existing file
234                        boolean deleted = deleteFile(toName);
235                        if (!deleted) {
236                            // if we could not delete any existing file then maybe the folder is missing
237                            // build folder if needed
238                            int lastPathIndex = toName.lastIndexOf('/');
239                            if (lastPathIndex != -1) {
240                                String directory = toName.substring(0, lastPathIndex);
241                                if (!SftpUtils.buildDirectory(channel, directory)) {
242                                    log.warn("Can not build directory: " + directory + " (maybe because of denied permissions)");
243                                }
244                            }
245                        }
246    
247                        // try to rename
248                        try {
249                            channel.rename(fromName, toName);
250                        } catch (SftpException e) {
251                            // ignore just log a warning
252                            log.warn("Can not move file: " + fromName + " to: " + toName);
253                        }
254                    }
255                }
256            }
257        }
258    
259        private boolean deleteFile(String filename) {
260            try {
261                channel.rm(filename);
262                return true;
263            } catch (SftpException e) {
264                // ignore just log a warning
265                log.warn("Could not delete file: " + filename + " from: " + remoteServer());
266                return false;
267            }
268        }
269    
270        protected void acquireExclusiveReadLock(ChannelSftp.LsEntry sftpFile) throws SftpException {
271            if (log.isTraceEnabled()) {
272                log.trace("Waiting for exclusive read lock to file: " + sftpFile);
273            }
274    
275            // the trick is to try to rename the file, if we can rename then we have exclusive read
276            // since its a remote file we can not use java.nio to get a RW access
277            String originalName = sftpFile.getFilename();
278            String newName = originalName + ".camelExclusiveReadLock";
279            boolean exclusive = false;
280            while (!exclusive) {
281                try {
282                    channel.rename(originalName, newName);
283                    exclusive = true;
284                } catch (SftpException e) {
285                    // ignore we can not rename it
286                }
287    
288                if (exclusive) {
289                    if (log.isDebugEnabled()) {
290                        log.debug("Acquired exclusive read lock to file: " + originalName);
291                    }
292                    // rename it back so we can read it
293                    channel.rename(newName, originalName);
294                } else {
295                    log.trace("Exclusive read lock not granted. Sleeping for 1000 millis");
296                    try {
297                        Thread.sleep(1000);
298                    } catch (InterruptedException e) {
299                        // ignore
300                    }
301                }
302            }
303        }
304    
305        protected String getFileName(Object file) {
306            ChannelSftp.LsEntry sftpFile = (ChannelSftp.LsEntry) file;
307            return sftpFile.getFilename();
308        }
309    
310    }