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 */
017package org.apache.commons.vfs2.provider.ftp;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022
023import org.apache.commons.logging.Log;
024import org.apache.commons.logging.LogFactory;
025import org.apache.commons.net.ftp.FTPClient;
026import org.apache.commons.net.ftp.FTPFile;
027import org.apache.commons.net.ftp.FTPReply;
028import org.apache.commons.vfs2.FileSystemException;
029import org.apache.commons.vfs2.FileSystemOptions;
030import org.apache.commons.vfs2.UserAuthenticationData;
031import org.apache.commons.vfs2.provider.GenericFileName;
032import org.apache.commons.vfs2.util.UserAuthenticatorUtils;
033
034/**
035 * A wrapper to the FTPClient to allow automatic reconnect on connection loss.
036 * <p>
037 * I decided to not to use eg. noop() to determine the state of the connection to avoid unnecessary server round-trips.
038 */
039public class FTPClientWrapper implements FtpClient {
040
041    private static final Log LOG = LogFactory.getLog(FTPClientWrapper.class);
042
043    protected final FileSystemOptions fileSystemOptions;
044    private final GenericFileName root;
045    private FTPClient ftpClient;
046
047    protected FTPClientWrapper(final GenericFileName root, final FileSystemOptions fileSystemOptions)
048            throws FileSystemException {
049        this.root = root;
050        this.fileSystemOptions = fileSystemOptions;
051        getFtpClient(); // fail-fast
052    }
053
054    public GenericFileName getRoot() {
055        return root;
056    }
057
058    public FileSystemOptions getFileSystemOptions() {
059        return fileSystemOptions;
060    }
061
062    private FTPClient createClient() throws FileSystemException {
063        final GenericFileName rootName = getRoot();
064
065        UserAuthenticationData authData = null;
066        try {
067            authData = UserAuthenticatorUtils.authenticate(fileSystemOptions, FtpFileProvider.AUTHENTICATOR_TYPES);
068
069            return createClient(rootName, authData);
070        } finally {
071            UserAuthenticatorUtils.cleanup(authData);
072        }
073    }
074
075    protected FTPClient createClient(final GenericFileName rootName, final UserAuthenticationData authData)
076            throws FileSystemException {
077        return FtpClientFactory.createConnection(rootName.getHostName(), rootName.getPort(),
078                UserAuthenticatorUtils.getData(authData, UserAuthenticationData.USERNAME,
079                        UserAuthenticatorUtils.toChar(rootName.getUserName())),
080                UserAuthenticatorUtils.getData(authData, UserAuthenticationData.PASSWORD,
081                        UserAuthenticatorUtils.toChar(rootName.getPassword())),
082                rootName.getPath(), getFileSystemOptions());
083    }
084
085    private FTPClient getFtpClient() throws FileSystemException {
086        if (ftpClient == null) {
087            ftpClient = createClient();
088        }
089
090        return ftpClient;
091    }
092
093    @Override
094    public boolean isConnected() throws FileSystemException {
095        return ftpClient != null && ftpClient.isConnected();
096    }
097
098    @Override
099    public void disconnect() throws IOException {
100        try {
101            getFtpClient().quit();
102        } catch (final IOException e) {
103            LOG.debug("I/O exception while trying to quit, probably it's a timed out connection, ignoring.", e);
104        } finally {
105            try {
106                getFtpClient().disconnect();
107            } catch (final IOException e) {
108                LOG.warn("I/O exception while trying to disconnect, probably it's a closed connection, ignoring.", e);
109            } finally {
110                ftpClient = null;
111            }
112        }
113    }
114
115    @Override
116    public FTPFile[] listFiles(final String relPath) throws IOException {
117        try {
118            // VFS-210: return getFtpClient().listFiles(relPath);
119            final FTPFile[] files = listFilesInDirectory(relPath);
120            return files;
121        } catch (final IOException e) {
122            disconnect();
123            final FTPFile[] files = listFilesInDirectory(relPath);
124            return files;
125        }
126    }
127
128    private FTPFile[] listFilesInDirectory(final String relPath) throws IOException {
129        FTPFile[] files;
130
131        // VFS-307: no check if we can simply list the files, this might fail if there are spaces in the path
132        files = getFtpClient().listFiles(relPath);
133        if (FTPReply.isPositiveCompletion(getFtpClient().getReplyCode())) {
134            return files;
135        }
136
137        // VFS-307: now try the hard way by cd'ing into the directory, list and cd back
138        // if VFS is required to fallback here the user might experience a real bad FTP performance
139        // as then every list requires 4 ftp commands.
140        String workingDirectory = null;
141        if (relPath != null) {
142            workingDirectory = getFtpClient().printWorkingDirectory();
143            if (!getFtpClient().changeWorkingDirectory(relPath)) {
144                return null;
145            }
146        }
147
148        files = getFtpClient().listFiles();
149
150        if (relPath != null && !getFtpClient().changeWorkingDirectory(workingDirectory)) {
151            throw new FileSystemException("vfs.provider.ftp.wrapper/change-work-directory-back.error",
152                    workingDirectory);
153        }
154        return files;
155    }
156
157    @Override
158    public boolean removeDirectory(final String relPath) throws IOException {
159        try {
160            return getFtpClient().removeDirectory(relPath);
161        } catch (final IOException e) {
162            disconnect();
163            return getFtpClient().removeDirectory(relPath);
164        }
165    }
166
167    @Override
168    public boolean deleteFile(final String relPath) throws IOException {
169        try {
170            return getFtpClient().deleteFile(relPath);
171        } catch (final IOException e) {
172            disconnect();
173            return getFtpClient().deleteFile(relPath);
174        }
175    }
176
177    @Override
178    public boolean rename(final String oldName, final String newName) throws IOException {
179        try {
180            return getFtpClient().rename(oldName, newName);
181        } catch (final IOException e) {
182            disconnect();
183            return getFtpClient().rename(oldName, newName);
184        }
185    }
186
187    @Override
188    public boolean makeDirectory(final String relPath) throws IOException {
189        try {
190            return getFtpClient().makeDirectory(relPath);
191        } catch (final IOException e) {
192            disconnect();
193            return getFtpClient().makeDirectory(relPath);
194        }
195    }
196
197    @Override
198    public boolean completePendingCommand() throws IOException {
199        if (ftpClient != null) {
200            return getFtpClient().completePendingCommand();
201        }
202
203        return true;
204    }
205
206    @Override
207    public InputStream retrieveFileStream(final String relPath) throws IOException {
208        try {
209            return getFtpClient().retrieveFileStream(relPath);
210        } catch (final IOException e) {
211            disconnect();
212            return getFtpClient().retrieveFileStream(relPath);
213        }
214    }
215
216    @Override
217    public InputStream retrieveFileStream(final String relPath, final long restartOffset) throws IOException {
218        try {
219            final FTPClient client = getFtpClient();
220            client.setRestartOffset(restartOffset);
221            return client.retrieveFileStream(relPath);
222        } catch (final IOException e) {
223            disconnect();
224            final FTPClient client = getFtpClient();
225            client.setRestartOffset(restartOffset);
226            return client.retrieveFileStream(relPath);
227        }
228    }
229
230    @Override
231    public OutputStream appendFileStream(final String relPath) throws IOException {
232        try {
233            return getFtpClient().appendFileStream(relPath);
234        } catch (final IOException e) {
235            disconnect();
236            return getFtpClient().appendFileStream(relPath);
237        }
238    }
239
240    @Override
241    public OutputStream storeFileStream(final String relPath) throws IOException {
242        try {
243            return getFtpClient().storeFileStream(relPath);
244        } catch (final IOException e) {
245            disconnect();
246            return getFtpClient().storeFileStream(relPath);
247        }
248    }
249
250    @Override
251    public boolean abort() throws IOException {
252        try {
253            // imario@apache.org: 2005-02-14
254            // it should be better to really "abort" the transfer, but
255            // currently I didnt manage to make it work - so lets "abort" the hard way.
256            // return getFtpClient().abort();
257
258            disconnect();
259            return true;
260        } catch (final IOException e) {
261            disconnect();
262        }
263        return true;
264    }
265
266    @Override
267    public String getReplyString() throws IOException {
268        return getFtpClient().getReplyString();
269    }
270}