Java tutorial
/* * Copyright 2015 Adaptris Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.adaptris.core.ftp; import static org.apache.commons.lang.StringUtils.isEmpty; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; import java.util.concurrent.TimeUnit; import javax.validation.Valid; import org.apache.commons.lang3.BooleanUtils; import com.adaptris.annotation.AdvancedConfig; import com.adaptris.annotation.InputFieldDefault; import com.adaptris.core.CoreException; import com.adaptris.core.NoOpConnection; import com.adaptris.filetransfer.FileTransferClient; import com.adaptris.filetransfer.FileTransferException; import com.adaptris.security.exc.PasswordException; import com.adaptris.util.NumberUtils; import com.adaptris.util.TimeInterval; import net.jodah.expiringmap.ExpirationListener; import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpiringMap; /** * Class containing common configuration for all FTP Connection types. * * @author lchan * @author $Author: lchan $ */ public abstract class FileTransferConnection extends NoOpConnection { /** * The default size of the cache if a size isn't specified ({@value #DEFAULT_MAX_CACHE_SIZE}) * * @see #setMaxClientCache(Integer) */ protected static final int DEFAULT_MAX_CACHE_SIZE = 16; protected static final TimeInterval DEFAULT_EXPIRATION = new TimeInterval(1L, TimeUnit.HOURS); private static final String UTF_8 = "UTF-8"; private String defaultUserName; @AdvancedConfig private Integer defaultControlPort; @AdvancedConfig @InputFieldDefault(value = "false") private Boolean forceRelativePath; @AdvancedConfig @InputFieldDefault(value = "false") private Boolean additionalDebug; @AdvancedConfig @InputFieldDefault(value = "false") private Boolean windowsWorkAround; @AdvancedConfig @InputFieldDefault(value = "false") private Boolean cacheConnection; @AdvancedConfig private Integer maxClientCacheSize; @AdvancedConfig @InputFieldDefault(value = "1 Hour") @Valid private TimeInterval cacheExpiration; private transient ExpiringMap<String, FileTransferClient> cachedConnections; /** * */ public FileTransferConnection() { } /** * Force the path to be relative when using {@linkplain #getDirectoryRoot(String)}. * <p> * This is useful in the situation where the server in question does not have the user in a ftp jail * </p> * * @param b true to prefix a <code>.</code> to the path. * @see #getDirectoryRoot(String) */ public void setForceRelativePath(Boolean b) { forceRelativePath = b; } /** * Get the force relative path flag. * * @return true or false. * @see #setForceRelativePath(Boolean) */ public Boolean getForceRelativePath() { return forceRelativePath; } public boolean forceRelativePath() { return BooleanUtils.toBooleanDefaultIfNull(getForceRelativePath(), false); } /** * <p> * Returns the default user name. * </p> * * @return the default user name */ public String getDefaultUserName() { return defaultUserName; } /** * Set the user name. * * @param s the username. */ public void setDefaultUserName(String s) { defaultUserName = s; } /** * @return Returns the defaultControlPort. */ public Integer getDefaultControlPort() { return defaultControlPort; } public abstract int defaultControlPort(); /** * Override the default port. * * @param i The defaultControlPort to set. */ public void setDefaultControlPort(Integer i) { defaultControlPort = i; } /** * Get additional logging output where available. * * @param b true to get additional logging. */ public void setAdditionalDebug(Boolean b) { additionalDebug = b; } /** * The additional debug flag. * * @see #setAdditionalDebug(Boolean) * @return true or false (default false) */ public Boolean getAdditionalDebug() { return additionalDebug; } public boolean additionalDebug() { return BooleanUtils.toBooleanDefaultIfNull(getAdditionalDebug(), false); } /** * @return the windowsWorkAround * @see #setWindowsWorkAround(Boolean) */ public Boolean getWindowsWorkAround() { return windowsWorkAround; } /** * Set whether the target server is a windows machine that returns backslash separated filenames when doing NLIST on a directory * rather than the normal forward slashes. * * @param b the windowsWorkAround to set */ public void setWindowsWorkAround(Boolean b) { windowsWorkAround = b; } public boolean windowsWorkaround() { return BooleanUtils.toBooleanDefaultIfNull(getWindowsWorkAround(), false); } /** * @return whether connection is held open after use */ public Boolean getCacheConnection() { return cacheConnection; } /** * Set whether or not connections created are held open for future use. * <p> * This feature is primarily intended to mitigate the connection cost when using {@linkplain FtpConsumer} with a frequent poll * interval.If multiple components end up using the same {@linkplain FileTransferClient} instance from the cache (perhaps you have * multiple FtpConsumer instances configured in the same channel) then you will likely end up with non-optimal performance due to * thread synchronisation. * </p> * <p> * It is generally recommended that you configure the associated {@linkplain FtpConsumer} and {@linkplain FtpProducer} instances * using the URL form of the destination if you intend to use the caching feature; this allows you to make sure that each * component has its own unique FileTransferClient instance associated with it. * </p> * * @param b true to enable, default false. * @see #setMaxClientCache(Integer) */ public void setCacheConnection(Boolean b) { cacheConnection = b; } public boolean cacheConnection() { return BooleanUtils.toBooleanDefaultIfNull(getCacheConnection(), false); } /** * * @see com.adaptris.core.NullConnection#initConnection() */ @Override protected void initConnection() throws CoreException { if (defaultUserName == null) { log.warn("No default user name, expected to be provided by destination"); } cachedConnections = ExpiringMap.builder().maxSize(maxClientCacheSize()) .asyncExpirationListener(new ExpiredClientListener()).expirationPolicy(ExpirationPolicy.ACCESSED) .expiration(expirationMillis(), TimeUnit.MILLISECONDS).build(); } /** * * @see com.adaptris.core.AdaptrisConnectionImp#closeConnection() */ @Override protected void closeConnection() { for (FileTransferClient client : cachedConnections.values()) { forceDisconnect(client); } cachedConnections.clear(); } /** * Connect to the host. * * @param hostUrl the host to connect to which can be in the form of an url or simply just the hostname in which case the default * credentials and port numbers are used. * @return an FtpClient that is ready to use. */ public FileTransferClient connect(String hostUrl) throws FileTransferException, IOException, PasswordException { FileTransferClient client = lookup(hostUrl); if (client == null) { client = create(hostUrl); } addToCache(hostUrl, client); return client; } private FileTransferClient create(String hostUrl) throws FileTransferException, IOException, PasswordException { String remoteHost = hostUrl; UserInfo ui = createUserInfo(); int port = defaultControlPort(); try { URI uri = new URI(hostUrl); if (acceptProtocol(uri.getScheme())) { remoteHost = uri.getHost(); port = uri.getPort() != -1 ? uri.getPort() : defaultControlPort(); ui.parse(uri.getRawUserInfo()); } } catch (URISyntaxException e) { ; } FileTransferClient client = create(remoteHost, port, ui); return client; } private FileTransferClient lookup(String hostUrl) { FileTransferClient result = null; if (cacheConnection()) { result = cachedConnections.get(hostUrl); if (result != null && result.isConnected()) { log.trace("Reusing an existing FileTransferClient for {}", hostUrl); } else { result = null; } } return result; } private void addToCache(String hostUrl, FileTransferClient client) { if (cacheConnection()) { cachedConnections.put(hostUrl, client); } } /** * Get the max number of entries in the cache. * * @return the maximum number of entries. */ public Integer getMaxClientCacheSize() { return maxClientCacheSize; } /** * Set the max number of entries in the cache. * <p> * Entries will be removed on a least recently accessed basis. * </p> * * @param maxSize the maximum number of entries, default is {@value #DEFAULT_MAX_CACHE_SIZE} * @see #setCacheConnection(Boolean) */ public void setMaxClientCache(Integer maxSize) { maxClientCacheSize = maxSize; } public int maxClientCacheSize() { return NumberUtils.toIntDefaultIfNull(getMaxClientCacheSize(), DEFAULT_MAX_CACHE_SIZE); } public TimeInterval getCacheExpiration() { return cacheExpiration; } public void setCacheExpiration(TimeInterval expiration) { this.cacheExpiration = expiration; } public <T extends FileTransferConnection> T withCacheExpiration(TimeInterval i) { setCacheExpiration(i); return (T) this; } protected long expirationMillis() { return TimeInterval.toMillisecondsDefaultIfNull(getCacheExpiration(), DEFAULT_EXPIRATION); } /** * Validate the URL Protocol when a URL is used. * * @param s the URL Protocol * @return true if the URL protocol is acceptable to the concrete imp. */ protected abstract boolean acceptProtocol(String s); /** * Create an instance of the <code>FileTransferClient</code> for use with the producer or consumer. * * @param host the remote host. * @param port the port to connect to * @param ui a local UserInfo containing username and password * @return a <code>FileTransferClient</code> object * @throws IOException wrapping a general comms error. * @throws FileTransferException if a protocol specific exception occurred. * @throws PasswordException */ protected abstract FileTransferClient create(String host, int port, UserInfo ui) throws IOException, FileTransferException, PasswordException; /** * <p> * Returns the directory root for the passed host URL. * </p> * * @param hostUrl the host URL * @return the directory root for the passed host URL */ public String getDirectoryRoot(String hostUrl) { String result = ""; try { URI uri = new URI(hostUrl); if (acceptProtocol(uri.getScheme())) { result = uri.getPath() != null ? uri.getPath() : ""; } } catch (URISyntaxException e) { ; } if (forceRelativePath()) { result = "." + result; } return result; } /** * Disconnect the FTP client. * * @param ftp the ftp client implementation */ public void disconnect(FileTransferClient ftp) { if (!cacheConnection()) { forceDisconnect(ftp); } } private void forceDisconnect(FileTransferClient ftp) { try { if (ftp != null) { ftp.disconnect(); } } catch (Exception e) { log.warn("Can not execute the FTP quit command {}", e.getMessage()); } } protected abstract UserInfo createUserInfo() throws FileTransferException; public class UserInfo { private String user, password; protected UserInfo() { } protected UserInfo(String defaultUser) { user = defaultUser; } protected UserInfo(String defaultUser, String defaultPassword) throws FileTransferException { user = defaultUser; password = defaultPassword; } protected void parse(String fulluserpass) throws UnsupportedEncodingException { if (isEmpty(fulluserpass)) { return; } // at this point we know that there are no invalid chars, because we used getRawUserInfo() // But we want to use raw, because they might be pesky and have : in their password. int passindex = fulluserpass.indexOf(':'); if (passindex != -1) { user = URLDecoder.decode(fulluserpass.substring(0, passindex), UTF_8); password = URLDecoder.decode(fulluserpass.substring(passindex + 1), UTF_8); } else { user = URLDecoder.decode(fulluserpass, UTF_8); } } public String getUser() { return user; } public String getPassword() { return password; } } private class ExpiredClientListener implements ExpirationListener<String, FileTransferClient> { @Override public void expired(String key, FileTransferClient value) { forceDisconnect(value); } } }