001package com.hfg.util.io;
002
003import java.io.IOException;
004import java.net.InetAddress;
005import java.net.UnknownHostException;
006import java.util.ArrayList;
007import java.util.List;
008import java.util.StringTokenizer;
009
010import org.apache.commons.net.ftp.FTPClient;
011import org.apache.commons.net.ftp.FTPClientConfig;
012import org.apache.commons.net.ftp.FTPFile;
013import org.apache.commons.net.ftp.FTPHTTPClient;
014import org.apache.commons.net.ftp.FTPReply;
015
016import com.hfg.security.LoginCredentials;
017import com.hfg.units.TimeUnit;
018
019//------------------------------------------------------------------------------
020/**
021 * Delineates FTP files matching the specified path. If behind a firewall that
022 * requires the use of proxies, you will need to set <code>socksProxyHost</code>
023 * and <code>socksProxyPort</code> (Jakarta's commons-net which is used for FTP
024 * uses sockets). See <a href='http://java.sun.com/j2se/1.4.2/docs/guide/net/properties.html'>
025 * http://java.sun.com/j2se/1.4.2/docs/guide/net/properties.html</a> for proxy details.
026 * <p>
027 * This class depends on Jakarta's commons-net library (which depends on jakarta-oro).
028 * </p>
029 * @author J. Alex Taylor, hairyfatguy.com
030 */
031//------------------------------------------------------------------------------
032// com.hfg XML/HTML Coding Library
033//
034// This library is free software; you can redistribute it and/or
035// modify it under the terms of the GNU Lesser General Public
036// License as published by the Free Software Foundation; either
037// version 2.1 of the License, or (at your option) any later version.
038//
039// This library is distributed in the hope that it will be useful,
040// but WITHOUT ANY WARRANTY; without even the implied warranty of
041// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
042// Lesser General Public License for more details.
043//
044// You should have received a copy of the GNU Lesser General Public
045// License along with this library; if not, write to the Free Software
046// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
047//
048// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
049// jataylor@hairyfatguy.com
050//------------------------------------------------------------------------------
051
052
053public class FTPRemoteFileLister  extends AbstractRemoteFileLister<FTPRemoteFile>
054{
055
056   //***************************************************************************
057   // PRIVATE FIELDS
058   //***************************************************************************
059
060   private List<FTPRemoteFile> mRemoteFiles;
061
062   private String       mFTPServer;
063   private Integer      mFTPPort;
064   private String       mProxyHost;
065   private Integer      mProxyPort = 80;
066   private String       mProxyUser;
067   private String       mProxyPassword;
068
069   private List<String> mFilePathPieces;
070   private FTPClient    mFTP;
071
072   private static final String PROTOCOL = "ftp";
073   private static LoginCredentials sDefaultCredentials = new LoginCredentials("anonymous", getUserAtHostPassword().toCharArray());
074
075   //***************************************************************************
076   // CONSTRUCTORS
077   //***************************************************************************
078
079   //---------------------------------------------------------------------------
080   public FTPRemoteFileLister()
081   {
082      super();
083      init();
084   }
085
086   //---------------------------------------------------------------------------
087   public FTPRemoteFileLister(String inFilePath)
088   {
089      super(inFilePath);
090      init();
091   }
092
093   //---------------------------------------------------------------------------
094   public FTPRemoteFileLister(String inFilePath, List<RemoteFileFilter> inFilterList)
095   {
096      super(inFilePath, inFilterList);
097      init();
098   }
099
100   //---------------------------------------------------------------------------
101   private void init()
102   {
103      setCredentials(sDefaultCredentials);
104   }
105
106   //***************************************************************************
107   // PUBLIC METHODS
108   //***************************************************************************
109
110   //---------------------------------------------------------------------------
111   @Override
112   public String getProtocol()
113   {
114      return PROTOCOL;
115   }
116
117   //---------------------------------------------------------------------------
118   @Override
119   public void setFilePath(String inFilePath)
120   {
121      super.setFilePath(inFilePath);
122      mRemoteFiles = null;
123   }
124
125   //---------------------------------------------------------------------------
126   public FTPClient getFTPClient()
127   {
128      if (null == mFTP
129          || ! mFTP.isConnected())
130      {
131         if (null == getCredentials())
132         {
133            throw new RuntimeException("No login credentials specified!?");
134         }
135
136         if (mProxyHost != null)
137         {
138            mFTP = new FTPHTTPClientWrapper(mProxyHost, mProxyPort, mProxyUser, mProxyPassword);
139         }
140         else
141         {
142            mFTP = new FTPClientWrapper();
143         }
144
145         mFTP.setRemoteVerificationEnabled(false); // Bad with caching firewalls
146         mFTP.setDataTimeout((int)TimeUnit.hour.getMilliseconds());
147
148         try
149         {
150            int reply;
151            if (mFTPPort != null)
152            {
153               mFTP.connect(mFTPServer, mFTPPort);
154            }
155            else
156            {
157               mFTP.connect(mFTPServer);
158            }
159
160            // After connection attempt, check the reply code to verify success.
161            reply = mFTP.getReplyCode();
162
163            if(! FTPReply.isPositiveCompletion(reply))
164            {
165               throw new RuntimeException("FTP server '" + mFTPServer + "' refused connection.");
166            }
167
168            mFTP.login(getCredentials().getUser(), new String(getCredentials().getPassword()));
169
170            // Hmmm. Data connections on my mac never seem to go anywhere unless I do this to force passive mode.
171            mFTP.enterLocalPassiveMode();
172         }
173         catch(IOException e)
174         {
175            if(mFTP.isConnected())
176            {
177               try
178               {
179                  mFTP.disconnect();
180               }
181               catch(IOException f)
182               {
183                  // do nothing
184               }
185            }
186
187            throw new RuntimeException(e);
188         }
189      }
190
191      return mFTP;
192   }
193
194
195   //---------------------------------------------------------------------------
196   public void clearRemoteFileList()
197   {
198      mRemoteFiles = null;
199   }
200
201   //---------------------------------------------------------------------------
202   public void logout()
203   throws IOException
204   {
205      if (mFTP != null) mFTP.logout();
206   }
207
208
209   //---------------------------------------------------------------------------
210   public FTPRemoteFileLister setProxyHost(String inValue)
211   {
212      mProxyHost = null;
213      if (inValue != null)
214      {
215         if (inValue.indexOf(":") > 0)
216         {
217            String[] pieces = inValue.split(":");
218            mProxyHost = pieces[0];
219            setProxyPort(Integer.parseInt(pieces[1]));
220         }
221         else
222         {
223            mProxyHost = inValue;
224         }
225      }
226      return this;
227   }
228
229   //---------------------------------------------------------------------------
230   public String getProxyHost()
231   {
232      return mProxyHost;
233   }
234
235   //---------------------------------------------------------------------------
236   public FTPRemoteFileLister setProxyPort(int inValue)
237   {
238      mProxyPort = inValue;
239      return this;
240   }
241
242   //---------------------------------------------------------------------------
243   public Integer getProxyPort()
244   {
245      return mProxyPort;
246   }
247
248   //---------------------------------------------------------------------------
249   public FTPRemoteFileLister setProxyUser(String inValue)
250   {
251      mProxyUser = inValue;
252      return this;
253   }
254
255   //---------------------------------------------------------------------------
256   public String getProxyUser()
257   {
258      return mProxyUser;
259   }
260
261   //---------------------------------------------------------------------------
262   public FTPRemoteFileLister setProxyPassword(String inValue)
263   {
264      mProxyPassword = inValue;
265      return this;
266   }
267
268   //---------------------------------------------------------------------------
269   public String getProxyPassword()
270   {
271      return mProxyPassword;
272   }
273
274   //***************************************************************************
275   // PROTECTED METHODS
276   //***************************************************************************
277
278   //---------------------------------------------------------------------------
279   protected List<FTPRemoteFile> getUnfilteredRemoteFileListImpl()
280   {
281      if (null == mRemoteFiles)
282      {
283        mFilePathPieces = splitFilePath();
284
285        // "" instead of "/" to start the recursion seems to cause trouble for some FTP servers.
286//        mRemoteFiles = recursiveFileLister(0, "");
287        mRemoteFiles = recursiveFileLister(0, "/");
288      }
289
290      return mRemoteFiles;
291   }
292
293   //***************************************************************************
294   // PRIVATE METHODS
295   //***************************************************************************
296
297   //---------------------------------------------------------------------------
298   private static String getUserAtHostPassword()
299   {
300      String passwd;
301      try
302      {
303         passwd = System.getProperty("user.name") + "@" + InetAddress.getLocalHost().getHostName();
304      }
305      catch (UnknownHostException e)
306      {
307         throw new RuntimeException(e);
308      }
309
310      return passwd;
311   }
312
313   //---------------------------------------------------------------------------
314   private List<String> splitFilePath()
315   {
316      // Remove 'ftp://'
317      String url = getFilePath().substring(6);
318      int index = url.indexOf("/");
319
320      // Remove (and save) the FTP server name
321      mFTPServer = url.substring(0, index);
322
323      int colonIndex = mFTPServer.indexOf(":");
324      if (colonIndex > 0)
325      {
326         mFTPPort = new Integer(mFTPServer.substring(colonIndex + 1));
327         mFTPServer = mFTPServer.substring(0, colonIndex);
328      }
329
330      url = url.substring(index + 1);
331
332      StringTokenizer st = new StringTokenizer(url, "/");
333
334      List<String> pieces = new ArrayList<String>();
335      while (st.hasMoreTokens())
336      {
337         pieces.add(st.nextToken());
338      }
339
340      return pieces;
341   }
342
343   //---------------------------------------------------------------------------
344   private List<FTPRemoteFile> recursiveFileLister(int inLevel, String inFilePath)
345   {
346      List<FTPRemoteFile> remoteFiles = new ArrayList<>();
347
348      if (!getFTPClient().isConnected()) throw new RuntimeException("Client not connected!");
349
350      try
351      {
352         if (inLevel < mFilePathPieces.size())
353         {
354            String levelString = mFilePathPieces.get(inLevel);
355
356            // Wildcard?
357            if (levelString.contains("*"))
358            {
359               // Get a list of the current dir.
360               FTPFile ftpFiles[] = getFTPClient().listFiles(inFilePath);
361               checkReplyCode("listFiles('" + inFilePath + "')");
362
363               if (ftpFiles != null)
364               {
365                  for (FTPFile file : ftpFiles)
366                  {
367                     if (file.isSymbolicLink()
368                           && file.getName().equals(inFilePath))
369                     {
370                        String adjustedFilePath = inFilePath.substring(0, inFilePath.lastIndexOf("/")) + "/" + file.getLink();
371                        remoteFiles.addAll(recursiveFileLister(inLevel, adjustedFilePath));
372                     }
373                     else if (wildcardMatch(levelString, file.getName()))
374                     {
375                        if (inLevel == mFilePathPieces.size() - 1)
376                        {
377                           remoteFiles.add(new FTPRemoteFile(mFTP, mFTPServer, inFilePath, file));
378                        }
379                        else
380                        {
381                           String filePath = inFilePath + "/" + file.getName();
382                           remoteFiles.addAll(recursiveFileLister(inLevel + 1, filePath));
383                        }
384                     }
385                  }
386               }
387            }
388            else
389            {
390               if (inLevel == mFilePathPieces.size() - 1)
391               {
392                  // Get a list of the current dir.
393                  FTPFile ftpFiles[] = getFTPClient().listFiles(inFilePath);
394                  checkReplyCode("listFiles('" + inFilePath + "')");
395
396                  if (ftpFiles != null)
397                  {
398                     for (FTPFile file : ftpFiles)
399                     {
400                        if (file != null  // Not sure why, but the file is sometimes null
401                            && levelString.equals(file.getName()))
402                        {
403                           if (file.isDirectory())
404                           {
405                              // Want to list the contents of the directory
406                              mFilePathPieces.add("*");
407                              String filePath = inFilePath + (inLevel > 0 ? "/" : "") + levelString;
408                              remoteFiles.addAll(recursiveFileLister(inLevel + 1, filePath));
409                           }
410                           else
411                           {
412                              if (file.getLink() != null)
413                              {
414                                 // Symbolic link.
415                                 // If we don't resolve the link, the correct file will get downloaded
416                                 // but the size won't match the size of the symbolic link.
417                                 FTPRemoteFile remoteFile = resolveLink(inFilePath, file)
418                                       .setRequestedPath(inFilePath + "/" + file.getName());
419                                 remoteFiles.add(remoteFile);
420                              }
421                              else
422                              {
423                                 remoteFiles.add(new FTPRemoteFile(mFTP, mFTPServer, inFilePath, file));
424                              }
425                           }
426                           break;
427                        }
428                     }
429                  }
430               }
431               else
432               {
433                  String filePath = inFilePath + (inLevel > 0 ? "/" : "") + levelString;
434                  remoteFiles.addAll(recursiveFileLister(inLevel + 1, filePath));
435               }
436            }
437
438         }
439      }
440      catch (IOException e)
441      {
442         throw new RuntimeException(e);
443      }
444
445      return remoteFiles;
446   }
447
448
449   //---------------------------------------------------------------------------
450   private FTPRemoteFile resolveLink(String inFilePath, FTPFile inLink)
451         throws IOException
452   {
453      String parentDirPath = inFilePath;
454      FTPFile file = inLink;
455      while (file.getLink() != null)
456      {
457         String filePath;
458         if (file.getLink().startsWith("/"))
459         {
460            filePath = file.getLink();
461         }
462         else // relative path
463         {
464            String linkPath = file.getLink();
465            while (linkPath.startsWith("."))
466            {
467               if (linkPath.startsWith("../"))
468               {
469                  parentDirPath = parentDirPath.substring(0, parentDirPath.lastIndexOf("/"));
470                  linkPath = linkPath.substring(3);
471               }
472               else if (linkPath.startsWith("./"))
473               {
474                  linkPath = linkPath.substring(2);
475               }
476            }
477
478            filePath = parentDirPath + "/" + linkPath;
479         }
480
481         file = getFTPClient().listFiles(filePath)[0];
482
483         parentDirPath = filePath.substring(0, filePath.lastIndexOf("/"));
484      }
485
486      return new FTPRemoteFile(mFTP, mFTPServer, parentDirPath, file);
487   }
488
489   //---------------------------------------------------------------------------
490   private void checkReplyCode(String inCmd)
491   {
492      int replyCode = getFTPClient().getReplyCode();
493      if(!FTPReply.isPositiveCompletion(replyCode))
494      {
495
496         throw new RuntimeException(inCmd + " returned: " + getFTPClient().getReplyString() + "!");
497      }
498   }
499
500   //***************************************************************************
501   // INNER CLASS
502   //***************************************************************************
503
504   // What is this for? It seems that if I call setFileType() repeatedly, FTPClient
505   // issues the FTP command even if the value of the file type did not change.
506   // This is also true for changeWorkingDirectory().
507   //---------------------------------------------------------------------------
508   private class FTPClientWrapper extends FTPClient
509   {
510      private int    mFileType = Integer.MIN_VALUE;
511      private String mConnectedHost;
512      private String mCurrentWorkingDirectory;
513
514      //---------------------------------------------------------------------------
515      public FTPClientWrapper()
516      {
517         super();
518
519         FTPClientConfig config = new FTPClientConfig(FTPClientConfig.SYST_UNIX);
520         // If the remote FTP server's time is slightly ahead of the local machine
521         // it can cause confusion because the year isn't typically set and the
522         // year will be guessed as last year.
523         config.setLenientFutureDates(true);
524         configure(config);
525      }
526
527      //---------------------------------------------------------------------------
528      @Override
529      public void connect(String inServer)
530      throws IOException
531      {
532         mConnectedHost = inServer;
533         super.connect(inServer);
534      }
535
536      //---------------------------------------------------------------------------
537      public String getConnectedHostname()
538      {
539         return mConnectedHost;
540      }
541
542      //---------------------------------------------------------------------------
543      @Override
544      public boolean setFileType(int inValue)
545      throws IOException
546      {
547         boolean result = true;
548         if (mFileType != inValue)
549         {
550            result = super.setFileType(inValue);
551            if (result)
552            {
553               mFileType = inValue;
554            }
555         }
556
557         return result;
558      }
559
560      //---------------------------------------------------------------------------
561      // There is no sense issuing a command to charge directories when we are already
562      // in the requested directory.
563      @Override
564      public boolean changeWorkingDirectory(String pathname)
565            throws IOException
566      {
567         boolean result = true;
568
569         if (null == mCurrentWorkingDirectory
570             || ! mCurrentWorkingDirectory.equals(pathname))
571         {
572            result = super.changeWorkingDirectory(pathname);
573            if (result)
574            {
575               mCurrentWorkingDirectory = pathname;
576            }
577         }
578
579         return result;
580      }
581   }
582
583   //***************************************************************************
584   // INNER CLASS
585   //***************************************************************************
586
587   // What is this for? It seems that if I call setFileType() repeatedly, FTPClient
588   // issues the FTP command even if the value of the file type did not change.
589   //---------------------------------------------------------------------------
590   private class FTPHTTPClientWrapper extends FTPHTTPClient
591   {
592      private int    mFileType = Integer.MIN_VALUE;
593      private String mConnectedHost;
594      private String mCurrentWorkingDirectory;
595
596
597      //---------------------------------------------------------------------------
598      public FTPHTTPClientWrapper(String inProxyHost, int inProxyPort, String inProxyUser, String inProxyPassword)
599      {
600         super(inProxyHost, inProxyPort, inProxyUser, inProxyPassword);
601
602         FTPClientConfig config = new FTPClientConfig(FTPClientConfig.SYST_UNIX);
603         // If the remote FTP server's time is slightly ahead of the local machine
604         // it can cause confusion because the year isn't typically set and the
605         // year will be guessed as last year.
606         config.setLenientFutureDates(true);
607         configure(config);
608      }
609
610      //---------------------------------------------------------------------------
611      @Override
612      public void connect(String inServer)
613      throws IOException
614      {
615         mConnectedHost = inServer;
616         super.connect(inServer);
617      }
618
619      //---------------------------------------------------------------------------
620      public String getConnectedHostname()
621      {
622         return mConnectedHost;
623      }
624
625      //---------------------------------------------------------------------------
626      @Override
627      public boolean setFileType(int inValue)
628      throws IOException
629      {
630         boolean result = true;
631         if (mFileType != inValue)
632         {
633            result = super.setFileType(inValue);
634            mFileType = inValue;
635         }
636
637         return result;
638      }
639
640      //---------------------------------------------------------------------------
641      // There is no sense issuing a command to charge directories when we are already
642      // in the requested directory.
643      @Override
644      public boolean changeWorkingDirectory(String pathname)
645            throws IOException
646      {
647         boolean result = true;
648
649         if (null == mCurrentWorkingDirectory
650             || ! mCurrentWorkingDirectory.equals(pathname))
651         {
652            result = super.changeWorkingDirectory(pathname);
653            if (result)
654            {
655               mCurrentWorkingDirectory = pathname;
656            }
657         }
658
659         return result;
660      }
661   }
662}