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}