package org.rr.commons.mufs; import java.io.IOException; import java.net.URL; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashMap; import java.util.Vector; import org.apache.commons.lang.StringUtils; import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient; public class FTPConnectionManager { private static final HashMap<String, FTPConnectionManager> sharedinstances = new HashMap<String, FTPConnectionManager>(); private final URL url; private final Vector<FTPClient> inPoolconnections = new Vector<>(); private final Vector<FTPClient> inUseConnections = new Vector<>(); /** * specifies the maxmimal number of connection to be established to one host. */ static final int MAX_CONNECTIONS = 10; private int connectionHighWaterMark = MAX_CONNECTIONS; private FTPConnectionManager(final URL url) { this.url = url; } /** * Get a new instance for the given URL. All {@link FTPConnectionManager} * having the same host in the url, will get the same shared instance. * * @param url The url for which the instance should be fetched. * @return The shared {@link FTPConnectionManager} instance. */ public static FTPConnectionManager getInstance(final URL url) { FTPConnectionManager connectionManager = sharedinstances.get(url.getHost().toLowerCase()); if(connectionManager==null) { connectionManager = new FTPConnectionManager(url); sharedinstances.put(url.getHost().toLowerCase(), connectionManager); } return connectionManager; } /** * Gets an connection which can be used exclusivly. This method can be used * for multithreaded tasks. Take sure that the connection is returned using * the {@link #releaseConnection(FTPClient)} method, so it can be reused. * * @return The connection. * @throws UnknownHostException * @throws ResourceHandlerException * @thorws RuntimeException */ public synchronized FTPClient getRegisteredConnection() throws UnknownHostException, ResourceHandlerException { FTPClient result = null; ArrayList<Exception> failcount = new ArrayList<>(); while(true) { //return an existing connection from the pool if(inPoolconnections.size() > 0) { //remove connection from pool and put it into the used list. result = inPoolconnections.remove(0); inUseConnections.add(result); try { initConnection(result, url); } catch (UnknownHostException e) { //host address seems to be invalid or unreachable. //no sense to retry disposeConnection(result); throw e; } catch (ResourceHandlerException e) { //login has failed, no sense to continue disposeConnection(result); throw e; } catch (Exception e) { //max connection limit reached? connectionHighWaterMark = inUseConnections.size(); failcount.add(e); } return result; } else if (inUseConnections.size() < connectionHighWaterMark) { //try to create a new connection if the highwatermark is not reached FTPClient newConnection = new FTPClient(); try { //init the new connection. this.initConnection(newConnection, url); } catch (UnknownHostException e) { //host address seems to be invalid or unreachable. //no sense to retry disposeConnection(result); throw e; } catch (ResourceHandlerException e) { //login has failed, no sense to continue disposeConnection(result); throw e; } catch (Exception e) { //max connection limit reached? connectionHighWaterMark = inUseConnections.size(); failcount.add(e); } this.inUseConnections.add(newConnection); return newConnection; } //test if the highwatermatk is 0. No sense to continue. if(connectionHighWaterMark == 0) { throw new RuntimeException("could not establish connection to " + String.valueOf(this.url)); } //test if there are too many errors occured and abort with the last //exception. if(failcount.size()>10) { throw new RuntimeException(failcount.get(failcount.size()-1)); } //wait a moment and retry. try { Thread.sleep(500); } catch (InterruptedException e) {} } } /** * Gets the highwatermark for the connections in the pool. * @return the highwater mark value. */ public int getConnectionHighWaterMark() { return this.connectionHighWaterMark; } /** * Give a connection, previously fetched using the {@link #getRegisteredConnection()} method, back, * so it can be provided again. * @param connection The connection to be given back. */ public void releaseConnection(final FTPClient connection) { this.inUseConnections.remove(connection); this.inPoolconnections.add(connection); } /** * Remove and close the given connection. * @param connection The connection to be removed. */ public void disposeConnection(final FTPClient connection) { //remove the connection from the caches. It's no longer provided. this.inUseConnections.remove(connection); this.inPoolconnections.remove(connection); //do the dispose in a new thread. It's not needed to //let the application wait till the dispose process has been finished. new Thread(new Runnable() { @Override public void run() { try { connection.logout(); } catch (Exception e) {} try { connection.disconnect(); } catch (Exception e) {} } }).start(); } /** * Initializes the given connection if not already done. * @param connection The connection to be initialized * @param url The url containing the host to be connected to. * @throws UnknownHostException * @throws IOException * @throws ResourceHandlerException if the login fails */ private void initConnection(final FTPClient connection, final URL url) throws UnknownHostException, IOException, ResourceHandlerException { connection.setDefaultTimeout(3000); if(!connection.isConnected()) { int port = 21; if(url.getPort() > 0) { port = url.getPort(); } connection.connect(url.getHost(), port); String userName = "anonymous"; String pass = "anonymous"; String userInfo = url.getUserInfo(); if(userInfo!=null && userInfo.length()>0) { String[] split = StringUtils.split(userInfo, ':'); if(split!=null) { if(split.length >= 1) { userName = split[0]; } if(split.length >= 2) { pass = split[1]; } } } boolean result = connection.login(userName, pass); if(!result) { throw new ResourceHandlerException("login for " + String.valueOf(userName) + "/" + String.valueOf(pass) + " has failed."); } } //reset to ascii connection.setFileType(FTP.ASCII_FILE_TYPE); } }