/* * Copyright (c) 2014 tabletoptool.com team. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: * rptools.com team - initial implementation * tabletoptool.com team - further development */ package com.t3.client.ui.io; /** * * @author crash */ import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.GridLayout; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.apache.log4j.Logger; import com.t3.client.ui.io.FTPTransferObject.Direction; public class FTPClient { private static final Logger log = Logger.getLogger(FTPClient.class); protected FTPClientConn cconn; protected List<Object> fifoQueue; protected Map<Object, FTPTransferObject> todoMap; // Todo list for uploads protected Map<Object, FTPTransferObject> transferringMap; // Currently in process... private int numThreads = 1; private List<ChangeListener> changeListeners; private boolean running; public FTPClient(String _host, String _user, String _password) { cconn = new FTPClientConn(_host, _user, _password); fifoQueue = new LinkedList<Object>(); todoMap = new HashMap<Object, FTPTransferObject>(); transferringMap = new HashMap<Object, FTPTransferObject>(); } /** * <p> * This method enables or disables the processing of transfer objects in the * queue. * </p> * <p> * If this method is called with a <code>true</code> parameter, the object * begins processing FTP requests as soon as they are added to the queue, up * to the maximum number of simultaneous transfers as set by a call to * {@link #setNumberOfThreads(int)}. * </p> * <p> * If this method is called with a <code>false</code> parameter, transfer * requests will be added to the queue but no transfers in the queue will be * started. If there are existing transfers already in progress, they will * continue but the queue will not be read to process additional ones. * <p/> * * @param b * whether this object should process transfer requests from the * queue */ public void setEnabled(boolean b) { boolean old = running; running = b; if (old != b && b == true) { // We just enabled this object from a disabled state, so start the first transfer startNextTransfer(); } } public synchronized void addChangeListener(ChangeListener listener) { getChangeListeners().add(listener); } public synchronized void removeChangeListener(ChangeListener listener) { getChangeListeners().remove(listener); } public synchronized List<ChangeListener> getChangeListeners() { if (changeListeners == null) changeListeners = new LinkedList<ChangeListener>(); return changeListeners; } /** * <p> * This method may be called by threads other than the EventDispatch thread, * so we use <code>SwingUtilities.invokeLater()</code> to handle running it * on the AWT EDT. * </p> * * @param data */ protected void fireStateChanged(final Object data) { if (SwingUtilities.isEventDispatchThread()) postAllChangeEvents(data); else SwingUtilities.invokeLater(new Runnable() { @Override public void run() { postAllChangeEvents(data); } }); } private void postAllChangeEvents(Object fto) { ChangeEvent ev = new ChangeEvent(fto); for (ChangeListener listener : getChangeListeners()) { listener.stateChanged(ev); } } public int mkdir(String dir) { return cconn.mkdir(dir); } public int remove(String filename) { return cconn.remove(filename); } public void setNumberOfThreads(int num) { if (num > 0) numThreads = num; } public int getNumberOfThreads() { return numThreads; } public void addToQueue(FTPTransferObject xfer) { synchronized (todoMap) { fifoQueue.add(xfer.local); todoMap.put(xfer.local, xfer); } /* * Could perhaps optimize this better by looking for GET vs. PUT jobs * and doing one or two GETs in parallel with one or two PUTs. Most of * time this application will be using PUT so it probably doesn't * matter. */ boolean startAnother = false; synchronized (transferringMap) { if (transferringMap.size() < numThreads) startAnother = true; } if (startAnother) startNextTransfer(); } public void removeFromQueue(Object local) { // First take it out of the todo list so it isn't started (we can't do an FTP ABORT // using the Sun JDK implementation). synchronized (todoMap) { fifoQueue.remove(local); todoMap.remove(local); } // Now check to see if it's running. If so, deleting it from the transferringMap means // we don't care about the results. synchronized (transferringMap) { transferringMap.remove(local); } } public void removeAllFromQueue() { /* * We can't actually stop an FTP transfer; we have to let it finish. * This is ugly. It means that we need to let the transfer finish and * remove the remote file, but we don't want to be displaying the * progress bar(s) in the mean time. The calling method must turn off * display of the dialog yet not remove the change changeListeners until * all outstanding transfer objects have been completed. Then those * objects should be removed from the server. */ synchronized (todoMap) { fifoQueue.clear(); todoMap.clear(); } synchronized (transferringMap) { transferringMap.clear(); } } private synchronized void startNextTransfer() { FTPTransferObject data; Object local; synchronized (todoMap) { if (!running || fifoQueue.isEmpty()) return; local = fifoQueue.remove(0); data = todoMap.remove(local); } synchronized (transferringMap) { transferringMap.put(local, data); } final FTPTransferObject thread_data = data; Thread th = new Thread("FTP " + data.remote) { @Override public void run() { doit(thread_data); } }; th.start(); } private void uploadDone(FTPTransferObject data, boolean keep) { boolean startAnother = false; synchronized (transferringMap) { if (transferringMap.containsKey(data.local)) transferringMap.remove(data.local); // TODO Should delete the remote file for uploading, or remove the local // file for downloading. if (fifoQueue.isEmpty() == false && transferringMap.size() < numThreads) startAnother = true; } if (startAnother) startNextTransfer(); else if (fifoQueue.isEmpty()) fireStateChanged(this); } private static final int BLOCKSIZE = 4 * 1024; protected InputStream prepareInputStream(FTPTransferObject data) { InputStream is = null; if (data.getput == Direction.FTP_PUT) { /* * In this situation, "data.local" is the InputStream. */ if (data.local instanceof byte[]) { is = new ByteArrayInputStream((byte[]) data.local); } else if (data.local instanceof ByteArrayInputStream) { is = (ByteArrayInputStream) data.local; } else if (data.local instanceof InputStream) { is = (InputStream) data.local; // System.err.println("is.available() = " + is.available()); } else if (data.local instanceof String) { File file = new File((String) data.local); try { data.setMaximum((int) ((file.length() + BLOCKSIZE - 1) / BLOCKSIZE)); fireStateChanged(data); return new FileInputStream(file); } catch (FileNotFoundException e) { log.error("Can't find local asset " + file, e); } } else { log.error("Illegal input object class: " + data.local.getClass()); } if (is instanceof ByteArrayInputStream) { data.setMaximum((((ByteArrayInputStream) is).available() + BLOCKSIZE - 1) / BLOCKSIZE); fireStateChanged(data); } } else { /* * In this situation, "data.remote" is the InputStream. */ try { is = cconn.openDownloadStream(data.remoteDir.getPath(), data.remote); } catch (IOException e) { File file = new File(data.remoteDir, data.remote); log.error("Attempting to open remote file " + file.getPath(), e); } } return is; } protected OutputStream prepareOutputStream(FTPTransferObject data) throws IOException { OutputStream os = null; if (data.getput == Direction.FTP_PUT) { /* * In this situation, "data.remote" is the OutputStream. */ // try { if (data.remoteDir != null) { cconn.mkdir(data.remoteDir.getPath()); os = cconn.openUploadStream(data.remoteDir.getPath(), data.remote); } else os = cconn.openUploadStream(data.remote); // } catch (IOException e) { // File file = new File(data.remoteDir, data.remote); // log.error("Attempting to FTP_PUT local asset " + file.getPath()); // e.printStackTrace(); // } } else { /* * In this situation, "data.local" is the OutputStream. */ if (data.local instanceof String) { File file = new File((String) data.local); try { os = new FileOutputStream(file); } catch (FileNotFoundException e) { log.error("Can't write local file " + file, e); } } else if (data.local instanceof OutputStream) { os = (OutputStream) data.local; } else { log.error("Illegal output object class: " + data.local.getClass()); throw new IllegalArgumentException("Cannot determine output type for " + data.local); } } return os; } protected void doit(FTPTransferObject data) { try { try (InputStream is = prepareInputStream(data); OutputStream os = prepareOutputStream(data);) { if (is == null || os == null) { log.error("Can't build connection"); return; } byte[] buf = new byte[BLOCKSIZE]; int c = 1; while (c > 0) { c = is.read(buf); if (c > 0) os.write(buf, 0, c); data.incrCurrentPosition(); fireStateChanged(data); } data.incrCurrentPosition(); } } catch (IOException e) { /* * For an IOException, it doesn't matter if it's a networking * problem or just that the FTP server doesn't like the commands * we're sending. Either way, we can mark this connection as dead * and skip any remaining transfers. Since we might be multithreaded * though, we just flag this client as "not usable" and let the * queue drain normally. */ log.error(e.getMessage(), e); setEnabled(false); } catch (Exception e) { log.error(e.getMessage(), e); } finally { uploadDone(data, false); } } public static void main(String args[]) { JFrame frame = new JFrame("FTP Test"); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.setLayout(new BorderLayout()); JLabel progress = new JLabel(); frame.add(progress, BorderLayout.SOUTH); JPanel panel = new JPanel(); panel.setLayout(new GridLayout(0, 1, 5, 5)); frame.add(panel, BorderLayout.CENTER); frame.setSize(new Dimension(400, 200)); frame.setVisible(true); String[] uploadList = new String[] { "campaignItemList.xml", "mockup.jfpr", "standard.mtprops", "updateRepoDialog.xml", }; FTPClient ftp = new FTPClient("www.eeconsulting.net", "username", "password"); // ftp.setNumberOfThreads(3); File dir = new File("testdir"); for (int i = 0; i < uploadList.length; i++) { FTPTransferObject fto = new FTPTransferObject(FTPTransferObject.Direction.FTP_PUT, uploadList[i], dir, uploadList[i]); ftp.addToQueue(fto); } // Need to listen for all progress bars to finish and count down using 'progress'. } }