package org.rr.commons.mufs; import static org.rr.commons.utils.StringUtil.EMPTY; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import org.apache.commons.io.IOUtils; import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPFile; public class FTPResourceHandler extends AResourceHandler { /** * The url to the resource. examply ftp://username:password@ftp.whatever.com/file.zip;type=i */ private URL ftpURL; /** * Contains the parent directory for this {@link FTPResourceHandler} instance. */ private FTPResourceHandler parentResourceHandler; private FTPFile ftpFile; /** * the cached child directories. */ private IResourceHandler[] childDirectories = null; /** * the cached child files. */ private IResourceHandler[] childFiles = null; /** * tells if this resource exists. <code>null</code> if this flag isn't initialized. */ private Boolean exists; private static Boolean apacheFTPAvailable = null; FTPResourceHandler() { super(); } public FTPResourceHandler(final String resource) throws IOException { super(); // ftp://username:password@ftp.whatever.com/file.zip;type=i try { String resourceString = resource; if(resourceString.indexOf(';')!=-1) { resourceString = resourceString.substring(0, resourceString.indexOf(';')); } this.ftpURL = new URL(resourceString); } catch (MalformedURLException e) { throw new RuntimeException("could not create FTPResourceHandler for " + resource, e); } } public FTPResourceHandler(URL ftpURL) throws IOException { super(); this.ftpURL = ftpURL; } public FTPResourceHandler(URL ftpURL, FTPResourceHandler parent, FTPFile ftpFile) { super(); this.ftpURL = ftpURL; this.ftpFile = ftpFile; } /** * Creates a new FTPResourceHandler instance. * * @throws IOException */ @Override public IResourceHandler createInstance(String resource) throws IOException { return new FTPResourceHandler(resource); } /** * Delete the resource handled by this {@link IResourceHandler} instance. * * @throws IOException * if the resource could not be deleted. */ @Override public void delete() throws IOException { final String path = getURLPath(this.ftpURL); try { FTPClient connection = this.getConnection(); try { boolean result; if (this.isDirectoryResource()) { result = connection.removeDirectory(path); } else { result = connection.deleteFile(path); } if (result == false) { throw new IOException("could not delete resource " + String.valueOf(ftpURL)); } // set the exists flag. The resource is deleted now. this.exists = Boolean.FALSE; } finally { releaseConnection(connection); } } catch (IOException e) { throw e; } catch (Exception e) { throw new IOException("could not delete resource " + String.valueOf(ftpURL), e); } } @Override public boolean moveToTrash() throws IOException { return ResourceHandlerUtils.moveToTrash(this); } /** * Frees all resources */ @Override public void dispose() { this.ftpFile = null; this.childDirectories = null; this.childFiles = null; this.exists = null; } /** * Tells if the resource handled by this {@link IResourceHandler} exists. * * @return <code>true</code> if the resource exists and <code>false</code> otherwise. */ @Override public boolean exists() { if (this.exists != null) { return this.exists.booleanValue(); } // the root should always exists if (this.getParentResource() == null) { this.exists = Boolean.TRUE; return true; } if (this.isDirectoryResource()) { this.exists = Boolean.TRUE; return true; } else { try { IResourceHandler[] listFileResources = this.getParentResource().listFileResources(); for (int i = 0; i < listFileResources.length; i++) { if (listFileResources[i].getName().equals(this.getName())) { this.exists = Boolean.TRUE; return true; } } } catch (IOException e) { this.exists = Boolean.FALSE; return false; } } this.exists = Boolean.FALSE; return false; } /** * Gets an {@link InputStream} for the resource handled by this {@link IResourceHandler} instance. * * @return The an {@link InputStream} to the resource. */ @Override public ResourceHandlerInputStream getContentInputStream() throws IOException { return new ResourceHandlerInputStream(this, new InputStream() { private InputStream retrieveFileStream = null; private FTPClient connection = null; /** * Creates and setup the stream to the ftp socket. */ { IOException lastException = null; for (int i = 0; i < FTPConnectionManager.MAX_CONNECTIONS + 1; i++) { connection = getConnection(); connection.setFileType(FTP.BINARY_FILE_TYPE); // the result can be null. thats the reason while handle this in a loop. try { retrieveFileStream = connection.retrieveFileStream(getURLPath(ftpURL)); } catch (IOException e) { retrieveFileStream = null; lastException = e; } if (retrieveFileStream != null) { break; } else { disposeConnection(connection); continue; } } if (retrieveFileStream == null && lastException != null) { throw lastException; } else if (retrieveFileStream == null) { throw new IOException("could not fetch input stream for connection " + ftpURL); } } @Override public int read() throws IOException { return retrieveFileStream.read(); } @Override public void close() throws IOException { try { // close stream IOUtils.closeQuietly(retrieveFileStream); // complete ftp protocoll transfer connection.completePendingCommand(); } finally { // give the connection back for reusing releaseConnection(connection); } } }); } @Override public OutputStream getContentOutputStream(final boolean append) throws IOException { if (!append && this.exists()) { this.delete(); } return new OutputStream() { private OutputStream storeFileStream = null; private FTPClient connection = null; /** * Creates and setup the stream to the ftp socket. */ { IOException lastException = null; for (int i = 0; i < FTPConnectionManager.MAX_CONNECTIONS + 1; i++) { connection = getConnection(); connection.setFileType(FTP.BINARY_FILE_TYPE); try { // the result can be null. thats the reason while handle this in a loop. if (append) { storeFileStream = connection.appendFileStream(getURLPath(ftpURL)); } else { storeFileStream = connection.storeFileStream(getURLPath(ftpURL)); } } catch (IOException e) { storeFileStream = null; lastException = e; } if (storeFileStream != null) { break; } else { disposeConnection(connection); continue; } } if (storeFileStream == null && lastException != null) { throw lastException; } else if (storeFileStream == null) { throw new IOException("could not fetch output stream for connection " + ftpURL); } } @Override public void flush() throws IOException { storeFileStream.flush(); } @Override public void write(int b) throws IOException { storeFileStream.write(b); } @Override public void close() throws IOException { try { // reset the exists flag. It should be testes again next time if the resource exists. exists = null; // flush the content try { storeFileStream.flush(); } catch (Exception e) { } // close stream IOUtils.closeQuietly(storeFileStream); // complete ftp protocoll transfer connection.completePendingCommand(); } finally { // give the connection back for reusing releaseConnection(connection); } } }; } @Override public String getName() { String path = getURLPath(this.ftpURL); String name = new File(path).getName(); if(name==null || name.trim().length()==0 && isRoot()) { return "/"; } return name; } @Override public IResourceHandler getParentResource() { if (this.parentResourceHandler == null) { try { URL parentURL = getParentURL(this.ftpURL); if (parentURL == null) { return null; } this.parentResourceHandler = new FTPResourceHandler(parentURL); } catch (Exception e) { throw new RuntimeException(e); } } return this.parentResourceHandler; } /** * Returns the resource String for this ftp resource. for example <code>ftp://BENUTZERNAME:PASSWORT@HOST:PORT/DateiMitPfadangabe</code> */ @Override public String getResourceString() { return this.ftpURL.toString(); } /** * Determines if the resource handled by this {@link FTPResourceHandler} is a directory resource which could have children. * * @return <code>true</code> if it's a dir or <code>false</code> otherwise. */ @Override public boolean isDirectoryResource() { if (getParentResource() == null) { return true; // it's the main directory } try { FTPFile file = getFTPFile(ftpURL); if (file == null) { return true; } return file.isDirectory(); } catch (Exception e) { //not exists //Logging.log(Level.WARNING, this, "error occures while find out if the resource is a directory resource for " + String.valueOf(ftpURL), e); return false; } } /** * Determines if the given resource string is an ftp resource string. An ftp resource String could look as follow: * <code>ftp://BENUTZERNAME:PASSWORT@HOST:PORT/DateiMitPfadangabe</code> */ @Override public boolean isValidResource(String resource) { if (resource.toLowerCase().startsWith("ftp://")) { if(!isApacheNetAvailable()) { return false; } try { new URL(resource); } catch (Exception e) { return false; } return true; } return false; } /** * Tests if the apache common net framework is available. * @return <code>true</code> if the framework is available and <code>false</code> * otherwise. */ private boolean isApacheNetAvailable() { if(apacheFTPAvailable != null) { return apacheFTPAvailable.booleanValue(); } try { Class.forName("org.apache.commons.net.ftp.FTP"); apacheFTPAvailable = Boolean.TRUE; return true; } catch (ClassNotFoundException e1) { apacheFTPAvailable = Boolean.FALSE; return false; } } @Override public IResourceHandler[] listDirectoryResources(ResourceNameFilter filter) throws IOException { if (this.childDirectories == null) { FTPClient connection = this.getConnection(); try { final FTPFile[] listDirectories = connection.listFiles(getURLPath(this.ftpURL)); ArrayList<IResourceHandler> result = new ArrayList<>(); for (int i = 0; i < listDirectories.length; i++) { if (listDirectories[i].isDirectory() && !listDirectories[i].getName().equals(".") && !listDirectories[i].getName().equals("..")) { FTPResourceHandler newResourceHandler = new FTPResourceHandler(addURLPath(this.ftpURL, listDirectories[i].getName()), this, listDirectories[i]); if(filter != null && filter.accept(newResourceHandler)) { result.add(newResourceHandler); } else if(filter == null) { result.add(newResourceHandler); } } } this.childDirectories = result.toArray(new IResourceHandler[result.size()]); } finally { releaseConnection(connection); } } return this.childDirectories; } @Override public IResourceHandler[] listFileResources() throws IOException { if (this.childFiles == null) { FTPClient connection = this.getConnection(); try { final FTPFile[] listDirectories = connection.listFiles(getURLPath(this.ftpURL)); ArrayList<IResourceHandler> result = new ArrayList<>(); for (int i = 0; i < listDirectories.length; i++) { if (listDirectories[i].isFile()) { FTPResourceHandler newResourceHandler = new FTPResourceHandler(addURLPath(this.ftpURL, listDirectories[i].getName()), this, listDirectories[i]); result.add(newResourceHandler); } } this.childFiles = result.toArray(new IResourceHandler[result.size()]); } finally { releaseConnection(connection); } } return this.childFiles; } @Override public boolean mkdirs() throws IOException { ArrayList<FTPResourceHandler> hierarchy = new ArrayList<>(); FTPResourceHandler parent = (FTPResourceHandler) this.getParentResource(); while (parent != null) { hierarchy.add(parent); parent = (FTPResourceHandler) parent.getParentResource(); } //reverse Arrays.sort(hierarchy.toArray(new FTPResourceHandler[hierarchy.size()]), Collections.reverseOrder()); FTPClient connection = this.getConnection(); for (int i = 0; i < hierarchy.size(); i++) { if (!hierarchy.get(i).exists()) { try { connection.makeDirectory(getURLPath(hierarchy.get(i).ftpURL)); } finally { releaseConnection(connection); } } } return true; } @Override public long size() { if (getParentResource() == null) { return 0; // it's the main directory } try { final FTPFile file = getFTPFile(ftpURL); if (file == null || file.isDirectory()) { return 0; } return file.getSize(); } catch (Exception e) { return 0; } } @Override public void writeStringContent(String content, String codepage) throws IOException { byte[] bytes = content.getBytes(Charset.forName(codepage)); OutputStream contentOutputStream = this.getContentOutputStream(false); IOUtils.write(bytes, contentOutputStream); contentOutputStream.flush(); IOUtils.closeQuietly(contentOutputStream); } @Override public void moveTo(IResourceHandler targetRecourceLoader, boolean overwrite) throws IOException { // handle overwrite if (!overwrite && targetRecourceLoader.exists()) { return; } if (targetRecourceLoader instanceof FTPResourceHandler) { // test if the source and the target are on the same host. if (ftpURL.getHost().equals(((FTPResourceHandler) targetRecourceLoader).ftpURL.getHost())) { FTPClient connection = this.getConnection(); try { connection.rename(getURLPath(this.ftpURL), getURLPath(((FTPResourceHandler) targetRecourceLoader).ftpURL)); } finally { releaseConnection(connection); } } } // use the stream move and delete if the resources are not at the same host super.moveTo(targetRecourceLoader, overwrite); } /** * clears the cache. The file listing must be reread if this is a directory resource. */ @Override public void refresh() { super.refresh(); this.childDirectories = null; this.childFiles = null; this.exists = null; } /** * @return <code>true</code> in any case because this is clarly a remote resource. */ public boolean isRemoteResource() { return true; } /** * better test for getting the parent url string for the root node * than creating the parent which is not needed. */ public boolean isRoot() { URL parentURL; try { parentURL = getParentURL(this.ftpURL); if (parentURL == null) { return true; } return false; } catch (MalformedURLException e) { throw new RuntimeException(e); } } private static URL getParentURL(URL source) throws MalformedURLException { String path = getURLPath(source); if (path.length() == 0 || path.equals("/")) { return null; // no parent available } String sourceString = source.toString(); String urlTrailer = EMPTY; if (sourceString.indexOf(';') != -1) { int semikolonIndex = sourceString.indexOf(';'); urlTrailer = sourceString.substring(semikolonIndex); sourceString = sourceString.substring(0, semikolonIndex); } String parentPath = new File(path).getParent(); String newUrlString = sourceString.replace(path, parentPath); return new URL(newUrlString + urlTrailer); } /** * add a folder or file to the url path * * @param source * The source url * @param add * The path or file statement to be added. * @return A new URL having the added path * @throws MalformedURLException */ private static URL addURLPath(URL source, String add) throws MalformedURLException { String sourceString = source.toString(); // remove the trailing string behind the path if there is one. String urlTrailer = EMPTY; if (sourceString.indexOf(';') != -1) { int semikolonIndex = sourceString.indexOf(';'); urlTrailer = sourceString.substring(semikolonIndex); sourceString = sourceString.substring(0, semikolonIndex); } if (sourceString.endsWith("/")) { sourceString += add; } else { sourceString += "/" + add; } sourceString += urlTrailer; return new URL(sourceString); } /** * Gets the remote path statement for this resource. for example "/pub/etc" from the url statement. * * @return The path statement. */ private static String getURLPath(URL url) { final String urlString = url.toString(); int pathStartIndex = 0; if(urlString.substring(6).indexOf('/')!=-1) { pathStartIndex = urlString.substring(6).indexOf('/') + 6; } else { pathStartIndex = urlString.length(); } int pathEndIndex = urlString.length(); if (urlString.indexOf(';') != -1) { pathEndIndex = urlString.indexOf(';'); } //could happens after cutting the ;value away if(pathStartIndex >= pathEndIndex) { return "/"; } String pathString = urlString.substring(pathStartIndex, pathEndIndex); return pathString; } /** * Give a connection, previously fetched using the {@link #getConnection()} method, back, so it can be provided again. * * @param connection * The connection to be given back. */ private void releaseConnection(final FTPClient connection) { if (connection != null) { FTPConnectionManager.getInstance(this.ftpURL).releaseConnection(connection); } } /** * Disposes the given connection if it's no longer needed. * * @param connection * The connection no longer used. */ private void disposeConnection(final FTPClient connection) { if (connection != null) { FTPConnectionManager.getInstance(this.ftpURL).disposeConnection(connection); } } /** * Gets a connection from the connection pool. Use {@link #releaseConnection(FTPClient)} for give it free. * * @return A ready to use connection. * @throws IOException * * @throws IOException */ private FTPClient getConnection() throws IOException { // get the connection manager instance final FTPConnectionManager ftpConnectionManager = FTPConnectionManager.getInstance(this.ftpURL); // find out how large is the pool and calculate the number of loops for getting a valid connection. int connectionPoolLoop = ftpConnectionManager.getConnectionHighWaterMark(); if (connectionPoolLoop <= 0) { connectionPoolLoop = FTPConnectionManager.MAX_CONNECTIONS; } connectionPoolLoop += 2; // try to get a valid connection FTPClient connectionFromPool = null; Exception lastException = null; for (int i = 0; i < connectionPoolLoop; i++) { try { connectionFromPool = FTPConnectionManager.getInstance(this.ftpURL).getRegisteredConnection(); boolean success = connectionFromPool.setFileType(FTP.ASCII_FILE_TYPE); if (!success) { throw new RuntimeException("Setting file type to ASCII has failed " + this); } return connectionFromPool; } catch (Exception e) { lastException = e; this.disposeConnection(connectionFromPool); continue; } } throw new IOException(lastException); } private FTPFile getFTPFile(URL url) throws IOException { if (this.ftpFile == null) { FTPClient connection = this.getConnection(); FTPFile[] listFiles = null; try { listFiles = connection.listFiles(getURLPath(this.ftpURL)); } finally { releaseConnection(connection); } if (listFiles != null) { for (int i = 0; i < listFiles.length; i++) { final String fileName = listFiles[i].getName().replaceAll("/", EMPTY); if (fileName.equals(this.getName())) { this.ftpFile = listFiles[i]; break; } } } } return this.ftpFile; } /** * The modification date of the resource. * * @return The modification date or <code>null</code> if something fails while reading the modification date. */ @Override public Date getModifiedAt() { try { return this.getFTPFile(this.ftpURL).getTimestamp().getTime(); } catch (IOException e) { return null; } } @Override public IResourceHandler addPathStatement(String statement) throws ResourceHandlerException { try { URL resultUrl = addURLPath(this.ftpURL, statement); return new FTPResourceHandler(resultUrl); } catch (Exception e) { throw new ResourceHandlerException(e); } } @Override public RESOURCE_HANDLER_USER_TYPES getType() { return RESOURCE_HANDLER_USER_TYPES.FTP; } }