Java tutorial
/* * The MHS-Collections Project shared library is intended for use by both the applet * and editor software in the interest of code consistency. * Copyright 2012-2013 Madeira Historical Society (developed by Brian Groenke) * * This library is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.madeirahs.shared.provider; import java.awt.image.*; import java.io.*; import java.net.*; import java.util.*; import java.util.concurrent.*; import javax.imageio.*; import org.apache.commons.net.ftp.*; /** * Implementation of DataProvider for obtaining data streams to/from a remote * FTP server. * * @author Brian Groenke * */ public class FTPProvider implements DataProvider { private static final int SO_TIMEOUT = 0x3938700, BUFF_SIZE = 0x4000; //16kB private FTPClient ftp = new FTPClient(); private String address, wkdir = "/", home = wkdir; private volatile boolean available, login; private Thread ka; private long lastPingTime; /** * All operations using FTPClient (with the exception for initializers) * should acquire a permit from Semaphore <code>guard</code> first. One * permit is available, and it is issued on a first-in-first-out basis. */ private Semaphore guard = new Semaphore(1, true); private final LogoutHook EXIT_HOOK = new LogoutHook(); /** * Connects to a FTP server without attempting a login. * * Note: this constructor simply calls: <code>connect(address)</code> * * @param address * address of the remote FTP server to connect to. * @throws SocketException * if the socket timeout could not be set. * @throws IOException * if the socket could not be opened. */ public FTPProvider(String address) throws SocketException, IOException { init(); connect(address); } /** * Create an FTPProvider that logs into and communicates with a remote FTP * server at the corresponding <code>address</code>, <code>username</code> * and <code>password</code>. * * Note: this constructor simply calls: <code>connect(address)</code> * * @param address * the remote address to connect to. * @param username * the username to login with. * @param password * the password to login with. * @throws LoginException * if the login was unsuccessful. * @throws SocketException * if the socket timeout could not be set. * @throws IOException * if the socket could not be opened. */ public FTPProvider(String address, String username, String password) throws LoginException, SocketException, IOException { init(); connect(address, username, password); } /** * Performs a connection attempt to the specified address without sending login information. * @param address * @throws SocketException * @throws IOException */ protected void connect(String address) throws SocketException, IOException { ftp.connect(address); int reply = ftp.getReplyCode(); if (!FTPReply.isPositiveCompletion(reply)) { throw (new IOException("connection attempted failed with reply code: " + reply)); } this.address = address; this.wkdir = ftp.printWorkingDirectory(); login = true; initConn(); } /** * Attempts to login to the FTP server at the specified address. * @param address * @param username * @param password * @throws SocketException * @throws IOException */ protected void connect(String address, String username, String password) throws SocketException, IOException { ftp.connect(address); int reply = ftp.getReplyCode(); if (!FTPReply.isPositiveCompletion(reply)) { throw (new IOException("connection attempted failed with reply code: " + reply)); } this.address = address; boolean success = ftp.login(username, password); if (!success) { ftp.disconnect(); throw (new LoginException("invalid login")); } else { Runtime.getRuntime().addShutdownHook(EXIT_HOOK); this.wkdir = ftp.printWorkingDirectory(); login = true; initConn(); } } /** * Internal initialization method called by constructors. This is called once before * any connection attempt is made. * @throws SocketException */ protected void init() throws IOException { ftp.setBufferSize(BUFF_SIZE); ftp.setSendBufferSize(BUFF_SIZE); ftp.setReceiveBufferSize(BUFF_SIZE); } /** * Internal connection initialization method called by connect methods. Subclasses should * also call this method if not explicitly calling super constructor. <br/> * <br/> * Specification:<br/> * Sets file transaction type to BINARY_FILE_TYPE, sets local passive mode * and launches FTP keepAlive thread. * * @throws IOException */ protected void initConn() throws IOException { ftp.setSoTimeout(SO_TIMEOUT); ftp.setTcpNoDelay(false); ftp.setFileTransferMode(FTPClient.BLOCK_TRANSFER_MODE); ftp.enterLocalPassiveMode(); // should help to avoid any issues with // firewalls ftp.setFileType(FTP.BINARY_FILE_TYPE); // All file transfers by // FTPProvider should be binary. ka = new Thread(new KeepAlive()); ka.setDaemon(true); ka.setName("Ftp_keepAlive"); ka.start(); } /** * Fetches the currently set working directory of the provider. The default * is usually just '/' representing the absolute root directory of the * system. * * @return the current working directory * @throws */ @Override public String getWorkingDir() { return wkdir; } /** * Fetches the home directory for this FTPProvider on the FTP server. * * @return the set home directory */ @Override public String getHome() { return home; } /** * Sets the working directory for this FTPProvider on the FTP server. * * @throws IOException * * @see #getWorkingDir() */ @Override public void setWorkingDir(String wkdir) throws IOException { try { guard.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } ftp.changeWorkingDirectory(wkdir); guard.release(); this.wkdir = wkdir; } @Override public String getProtocolName() { return "ftp"; } /** * Checks to see if this FTPProvider is connected, logged in and ready to be * used for data transfers. */ @Override public boolean isAvailable() { try { guard.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } boolean available = ftp.isConnected() && ftp.isAvailable() && login; guard.release(); return available; } /** * Fetches an input stream from the specified file on the server. Returns * null if the file doesn't exist, the stream can't be opened, or the server * sends a negative reply code. The returned InputStream will take care of * completing the transfer when you call <code>close()</code>. */ @Override public InputStream getInputStream(String fileName) throws IOException { try { guard.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } InputStream in = ftp.retrieveFileStream(fileName); guard.release(); if (in == null) { return null; } return new FtpInputStream(in); } /** * Fetches an output stream to the specified file on the server. Returns * null if the stream can't be opened, or the server sends a negative reply * code. The returned OutputStream will take care of completing the transfer * when you call <code>close()</code>. */ @Override public OutputStream getOutputStream(String fileName) throws IOException { try { guard.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } OutputStream out = ftp.storeFileStream(fileName); guard.release(); if (out == null) { return null; } return new FtpOutputStream(out); } /** * Fetches an image with the given name on the server. * @param fileName file name including location on server (relative to working dir). */ @Override public BufferedImage loadImage(String fileName) throws IOException { BufferedImage img = null; try { guard.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } InputStream in = ftp.retrieveFileStream(fileName); guard.release(); if (in == null) { return null; } in = new BufferedInputStream(new FtpInputStream(in)); img = ImageIO.read(in); in.close(); return img; } /** * Throws UnsupportedOperationException. DO NOT use with code that supports * an audio-enabled DataProvider. */ @Override public Object playAudio(String audioAddr) throws UnsupportedOperationException { throw (new UnsupportedOperationException("Audio playback is not supported by FTPProvider")); } /** * Logout and disconnect from the server. Note that all methods will most likely throw some sort of IOException * until <code>reconnect</code> is called. * @see #reconnect(String) * @see #reconnect(String,String,String) * @throws IOException */ public void disconnect() throws IOException { Runtime.getRuntime().removeShutdownHook(EXIT_HOOK); try { guard.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } ftp.logout(); ftp.disconnect(); guard.release(); login = false; ka.interrupt(); } /** * Reconnects this provider to a server. This does nothing if called * while the provider is already connected. * @param address * @throws SocketException * @throws IOException */ public void reconnect(String address) throws SocketException, IOException { if (!login) { connect(address); } } /** * Reconnects this provider to a server with a new login session. This does nothing if called * while the provider is already connected. * @param address * @param username * @param password * @throws SocketException * @throws IOException */ public void reconnect(String address, String username, String password) throws SocketException, IOException { if (!login) { connect(address, username, password); } } /** * Creates a directory at the specified path on the FTP server. * * @param dirPath * the fully qualified name of the new directory (including all * parent directories). * @return true if successful, false otherwise. */ public boolean mkdir(String dirPath) { try { guard.acquire(); ftp.makeDirectory(dirPath); guard.release(); return true; } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return false; } /** * Deletes a file at the specified pathname. * * @param pathname * fully qualified or relative pathname. * @return true if successful, false otherwise. */ public boolean delete(String pathname) { try { guard.acquire(); ftp.deleteFile(pathname); guard.release(); return true; } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return false; } /** * Attempts to remove a directory at the specified pathname. * * @param pathname * @return true if successful, false otherwise. */ public boolean rmdir(String pathname) { try { guard.acquire(); ftp.removeDirectory(pathname); guard.release(); return true; } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return false; } /** * Returns a list of file names in the given directory. * * @param dir * the directory whose contents to retrieve * @return array of file name strings in the 'dir' */ public String[] listNames(String dir) { String[] list = null; try { guard.acquire(); list = ftp.listNames(dir); guard.release(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return list; } /** * Returns a list of FTP files in the given directory. * * @param dir * the directory whose contents to retrieve * @return an FTPFile array representing the contents of 'dir' */ public FTPFile[] listFiles(String dir) { FTPFile[] files = null; try { guard.acquire(); files = ftp.listFiles(dir); guard.release(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return files; } @Override public boolean exists(String pathname) { try { guard.acquire(); String stat = ftp.getStatus(pathname); guard.release(); if (stat != null) { return true; } else { return false; } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return false; } /** * Note: accuracy of results for FTP servers are neither guaranteed nor * explicitly defined. The server may or may not provide the necessary file * size information and furthermore may update this information at its own * discretion. */ @Override public long sizeOf(String fileName) { try { guard.acquire(); FTPFile f = ftp.mlistFile(fileName); guard.release(); if (f == null) { return -1; } else { return f.getSize(); } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return -1; } /** * Renames a file at the specified pathname. * * @param fileName * path of the current file * @param newTarget * new path of the file * @return true if successful, false otherwise. */ @Override public boolean rename(String fileName, String newTarget) throws IOException { if (!exists(fileName)) { throw (new FileNotFoundException(fileName)); } try { guard.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } boolean success = ftp.rename(fileName, newTarget); guard.release(); return success; } /** * Sends a command to the FTP server and records the time it takes to * respond. * * @return the amount of time in milliseconds the server took to respond. * @throws IOException */ public long ping() throws IOException { synchronized (ftp) { try { guard.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } long t1 = System.currentTimeMillis(); ftp.feat(); long t2 = System.currentTimeMillis(); guard.release(); return t2 - t1; } } /** * Obtains the last modification time of the file. * @param fileName * @return a Calendar object representing the last modification time of the file in the local time zone. */ public Calendar getLastModified(String fileName) { synchronized (ftp) { try { guard.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } try { FTPFile file = ftp.mlistFile(fileName); Calendar time = file.getTimestamp(); time.setTimeZone(TimeZone.getDefault()); return time; } catch (IOException e) { e.printStackTrace(); } } return null; } /** * OutputStream returned by the <code>getOutputStream(String)</code> of this * class. It takes care of extra operations needed when closing the stream. * * @author Brian Groenke * */ protected class FtpOutputStream extends OutputStream { private OutputStream out; private boolean locked; public FtpOutputStream(OutputStream out) { if (out == null) { throw (new IllegalArgumentException("stream cannot be null")); } this.out = out; } @Override public void write(int b) throws IOException { if (!locked) { try { guard.acquire(); locked = true; } catch (InterruptedException e) { e.printStackTrace(); } } out.write(b); } @Override public void flush() throws IOException { out.flush(); } @Override public void close() throws IOException { out.close(); boolean fail = false; if (ftp == null || !ftp.completePendingCommand()) { fail = true; } guard.release(); locked = false; if (fail) throw (new IOException("failed to complete FTP transaction")); } } /** * InputStream returned by the <code>getInputStream(String)</code> of this * class. It takes care of extra operations needed when closing the stream. * * @author Brian Groenke * */ protected class FtpInputStream extends InputStream { private InputStream in; private boolean locked; public FtpInputStream(InputStream in) { this.in = in; } @Override public int read() throws IOException { if (!locked) { try { guard.acquire(); locked = true; } catch (InterruptedException e) { e.printStackTrace(); } } return in.read(); } @Override public void close() throws IOException { in.close(); boolean fail = false; if (ftp == null || !ftp.completePendingCommand()) { fail = true; } guard.release(); locked = false; if (fail) throw (new IOException("failed to complete FTP transaction")); } @Override public void mark(int limit) { in.mark(limit); } @Override public boolean markSupported() { return in.markSupported(); } @Override public void reset() throws IOException { in.reset(); } @Override public long skip(long n) throws IOException { return in.skip(n); } @Override public int available() throws IOException { return in.available(); } } private class LogoutHook extends Thread { public LogoutHook() { super(new Runnable() { @Override public void run() { try { login = false; ka.interrupt(); ftp.logout(); ftp.disconnect(); } catch (IOException e) { e.printStackTrace(); } } }); } } private class KeepAlive implements Runnable { public static final int INTERVAL = 0x493E0; // specified time in // milliseconds | currently // set to 5 mins (300,000ms) @Override public void run() { while (login) { try { ftp.setSoTimeout((int) (INTERVAL * 1.5)); lastPingTime = ping(); Thread.sleep(INTERVAL); // wait set amount of time - see // INTERVAL field above } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { // we really don't care } } } } }