001/*
002 * Version 0.70 01/04/2002
003 *
004 * Visit my url for update: http://www.geocities.com/beapetrovicova/
005 * 
006 * jFtp was developed by Bea Petrovicova <beapetrovicova@yahoo.com>.
007 * The design and implementation of jFtp are available for royalty-free 
008 * adoption and use. This software is provided 'as is' without any 
009 * guarantees. Copyright is retained by Bea Petrovicova. Redistribution 
010 * of any part of jFtp or any derivative works must include this notice.
011 *
012 */  
013
014package cz.dhl.ftp;
015
016import cz.dhl.io.CoFile;
017import cz.dhl.io.CoFilenameFilter;
018import cz.dhl.io.CoOrder;
019import cz.dhl.ui.CoConsole;
020import java.io.BufferedReader;
021import java.io.InputStreamReader;
022import java.io.InputStream;
023import java.io.IOException;
024import java.io.OutputStream; 
025import java.io.Reader;
026import java.text.DateFormat;
027import java.util.Calendar;
028import java.util.Date;
029import java.util.Locale;
030import java.util.NoSuchElementException;
031import java.util.StringTokenizer;
032import java.util.Vector;
033
034/**
035 * Allows uniform manipulation with FTP files.
036 * Equivalent for File object.
037 * 
038 * <P><B>Only absolute pathnames are supported!</B></P>
039 *
040 * @Version 0.70 01/04/2002
041 * @author Bea Petrovicova <beapetrovicova@yahoo.com>  
042 *
043 * @see Ftp
044 * @see cz.dhl.io.CoFile
045 * @see java.io.File
046 */
047public final class FtpFile
048   implements CoFile
049{
050   /* CoOrder Implementation. */
051   
052   private String name=null, ext=null;   
053
054   private void sortSetup(String name)
055   {  this.name=name.toUpperCase(); int index = this.name.lastIndexOf("."); 
056      if(index != -1 && index < this.name.length()) 
057         ext = this.name.substring(index); else ext = " " +this.name; }
058   
059   public int compareNameToIgnoreCase(CoOrder file)
060   {  if(file instanceof FtpFile)
061      {  FtpFile l2 = (FtpFile)file;
062         return name.compareTo(l2.name); }
063      else throw new ClassCastException(); }
064   
065   public int compareExtToIgnoreCase(CoOrder file)
066   {  if(file instanceof FtpFile)
067      {  FtpFile l2 = (FtpFile)file;
068         int result=ext.compareTo(l2.ext);
069         if(result==0)
070            result=name.compareTo(l2.name);
071         return result; }
072      else throw new ClassCastException(); }
073   
074   public boolean startsWithIgnoreCase(char ch)
075   {  return (name.charAt(0) == Character.toUpperCase(ch)); }
076   
077   public boolean equalsExtTo(String filter)
078   {  return (ext.compareTo(filter)==0); }
079   
080   public boolean equalsExtTo(String filter[])
081   {  boolean done = false;
082      for(int j=0;j<filter.length;j++)
083         if(ext.compareTo(filter[j])==0)
084            { done = true; break; }
085      return done; }
086      
087   public boolean equals(Object o)
088   {  if(o==null) return false; else return(compareTo(o)==0); }
089   
090   public int compareTo(Object o) 
091   {  String s1 = getHost()+getAbsolutePath(), s2;
092      if(o instanceof CoFile)
093      {  CoFile f2 = (CoFile)o;
094         s2 = f2.getHost()+f2.getAbsolutePath(); }
095      else if(o instanceof String)
096         s2 = (String)o;
097      else throw new ClassCastException();
098      return s1.compareTo(s2); }
099
100   public boolean isConnected() { return client.isConnected(); }
101
102   /* CoOrder Implementation. */
103
104   public char getDataType() 
105   {  if(client.getContext().getFileTransferMode() == 'S')
106         if(equalsExtTo(client.getContext().getTextFilter()))
107            return 'A'; else return 'I';
108      else return client.getContext().getFileTransferMode(); }
109      
110   public InputStream getInputStream() throws IOException
111      { return new FtpInputStream(this); }
112      
113   public OutputStream getOutputStream() throws IOException
114      { return new FtpOutputStream(this,false); } 
115      
116   public OutputStream getOutputStream(boolean append) throws IOException
117      { return new FtpOutputStream(this,append); }    
118      
119   public CoFile newFileChild(String child)
120      { return new FtpFile(this,child,this.client); }
121      
122   public CoFile newFileRename(String name)
123      { return new FtpFile(this.getParent(),name,this.client); }
124      
125   public CoConsole getConsole() 
126      { return client.getContext().getConsole(); }
127
128   /* CoFile Implementation. */
129      
130   private static final String months[] = {"JAN", "FEB", "MAR", 
131                                   "APR", "MAY", "JUN", 
132                                   "JUL", "AUG", "SEP", 
133                                   "OCT", "NOV", "DEC"};
134         
135   private static final char LINK = 'l';
136   private static final char SPECIAL = 'c';
137   private static final char FOLDER = 'd';
138   private static final char FILE = '-';
139
140   Ftp client = null;
141   
142   private String access = null;
143   private String owner = null;
144   private String group = null;
145   private long size = 0L;
146   private long date = 0L;
147   String path = null;
148
149   private FtpFile() {}
150   
151   /** Creates a new FtpFile instance by converting the 
152    * given pathname string into an abstract pathname. */
153   public FtpFile(String path, Ftp client) 
154   {  access = "d?????????";
155      owner  = ""; group  = "";
156      size   = -1; date   = 0L;
157      if(path.compareTo("/")!=0 && path.endsWith("/"))
158         this.path = path.substring(0,path.length()-1);
159      else this.path = path; 
160      this.client = client; 
161      sortSetup(getName());       
162   }
163   
164   /** Creates a new FtpFile instance from a parent 
165    * pathname string and a child pathname string. */
166   public FtpFile(String path, String name, Ftp client) 
167   {  access = "f?????????";
168      owner  = ""; group  = "";
169      size   = -1; date   = 0L;
170      if(path.endsWith("/"))
171         this.path = path+name; 
172      else this.path = path+"/"+name; 
173      this.client = client; 
174      sortSetup(name);       
175   }
176   
177   /** Creates a new FtpFile instance from a parent 
178    * abstract pathname and a child pathname string. */
179   public FtpFile(FtpFile dir, String name, Ftp client) 
180   {  this(dir.toString(),name,client); }
181   
182   public String getHost() 
183   {  String system = "";
184      try
185      {  system = client.host(); }
186      catch(IOException e)
187      {  client.getContext().printlog("< File: Can't obtain host name. >\n"+e); }
188      return system;
189   }
190
191   public String getAbsolutePath() { return path; }
192
193   public int getPathDepth()
194   {  int depth = -1; int length = -1;
195      while((length = path.indexOf('/',length + 1)) >= 0)
196         depth++;
197      if(!path.endsWith("/"))
198         depth++;
199      return depth;
200   }
201 
202   public CoFile getPathFragment(int depth) 
203   {  if(depth>0)
204      {  int length = -1;
205         for(int n=0;n<=depth;n++)
206            if((length = path.indexOf('/',length + 1)) < 0)
207               break;
208         if(length>0)
209            return new FtpFile(path.substring(0,length),client);
210         else return this;
211      } else return new FtpFile("/",client);
212   }
213   
214   public String[] getPathArray()
215   {  Vector dv = new Vector(); 
216      dv.addElement(getHost());
217      StringTokenizer toker = new StringTokenizer(path,"/");
218      while (true)
219         try
220         {  String d = toker.nextToken(); 
221            dv.addElement(d);
222         } catch(NoSuchElementException e)
223            { break; }
224      String[] ds = new String[dv.size()];
225      dv.copyInto(ds);
226      return ds;
227   }  
228
229   public String getName() 
230   {  if(path.lastIndexOf('/')>=0)
231         return path.substring(path.lastIndexOf('/')+1); 
232      else return path; }
233   
234   public String getParent() 
235   {  if(path.lastIndexOf('/')>0)
236         return path.substring(0,path.lastIndexOf('/')); 
237      else return new String("/"); }
238
239   public boolean delete() throws SecurityException
240   {  if(isDirectory())
241      {  /* release dir if current */
242         client.cd(getParent());
243         if(client.rmdir(path))
244            return true;
245         /* When using NAME_LIST, files/dirs 
246            are distinguished by guessing. */
247         else return client.rm(path);
248      } else if(client.rm(path))
249           return true;
250         else
251         {  /* release dir if current */
252            client.cd(getParent());
253            /* When using NAME_LIST, files/dirs 
254               are distinguished by guessing. */
255            return client.rmdir(path); }
256   }
257        
258   public boolean mkdir() throws SecurityException
259      { return client.mkdir(path); }
260   public boolean mkdirs() throws SecurityException
261   {  boolean done = true; 
262      int depth = getPathDepth();
263      for(int i=0; i<depth;i++)
264         if(!((FtpFile)getPathFragment(depth)).mkdir())
265            done = false;
266      return done;
267   }
268   public boolean renameTo(CoFile dest) throws SecurityException
269      { return client.mv(path,dest.getAbsolutePath()); }
270   
271   public long length() { return size; }
272   public long lastModified() { return date; }
273   public String lastModifiedString()
274   { return (DateFormat.getDateTimeInstance(
275        DateFormat.SHORT,DateFormat.SHORT)
276             ).format(new Date(lastModified())); }
277   
278   public boolean isAbsolute() { return (path.charAt(0) == '/'); }
279   public boolean isDirectory() { return (access.charAt(0) == FOLDER); }
280   public boolean isFile() { return (access.charAt(0) == FILE); }
281   public boolean isSpecial() { return (access.charAt(0) == SPECIAL); }
282   public boolean isLink() { return (access.charAt(0) == LINK); }
283   /** Tests if the file represented by this File object is executable. */
284   public boolean isExecutable() { return (access.indexOf('x') != -1); }
285   public boolean isHidden() { return false; }
286   public boolean canRead() { return true; }
287   public boolean canWrite() { return true; }
288   public boolean exists() { return false; }
289   
290   public String getAccess() { return access; };
291   /** Get owner of this file object. */
292   public String getOwner() { return owner; };
293   /** Get group of users for this file object. */
294   public String getGroup() { return group; };
295   public String propertyString()
296   {  String desc = getAccess()+" "+getOwner()+" "+getGroup();
297      return (isFile()?""+length()+" "+desc:desc); }  
298   
299   public CoFile[] listCoRoots()
300   {  CoFile fs[] = new CoFile[1];
301      fs[0] = getPathFragment(0);
302      return fs; }
303
304   public CoFile[] listCoFiles()
305      throws SecurityException
306   {  FtpFile[] fs = null; 
307      String line; 
308      BufferedReader ibuf  = null;
309
310      int listtype = client.getContext().getListCommandMode();
311      try
312      {  boolean error=false;
313         Vector fv = new Vector();
314
315         ibuf = new BufferedReader(new InputStreamReader(new FtpListInputStream(this)));
316         while ((line = ibuf.readLine()) != null)
317         {  try
318            {  switch(listtype)
319               {
320               case FtpContext.LIST:
321                  fv.addElement(FtpFile.examineListLine(this,line)); 
322                  break;
323               case FtpContext.NAME_LIST:
324                  if(line.startsWith("/") && line.endsWith(":"))
325                     break;
326                  if(line.indexOf("//")!=-1)
327                     line = line.substring(line.lastIndexOf('/')+1);
328               case FtpContext.NAME_LIST_LS_F:
329               case FtpContext.NAME_LIST_LS_P:
330                  if(line.length()>0)
331                     fv.addElement(FtpFile.examineNameListLine(this,line,listtype)); 
332                  break;
333               case FtpContext.NAME_LIST_LS_LA:
334                  fv.addElement(FtpFile.examineUnixListLine(this,line)); 
335                  break;
336               }
337            }
338            catch(NoSuchElementException e)
339            {  if(!error && (e.getMessage()==null || !e.getMessage().equals("skip")))
340               {  client.printlog("\n   < File: Invalid List Format ! >" +
341                                 (e.getMessage()!=null?"\n   "+e.getMessage():"")+
342                                 "\n   Line: "+line+
343                                 "\n   Try: 'NAME_LIST' List Command");
344                  error = true; } }
345         }
346         fs = new FtpFile[fv.size()];
347         fv.copyInto(fs);
348      }
349      catch(IOException e)
350         {  client.printlog("< File: Can't list directory! >\n"+e); }
351      finally
352      {  try 
353         {  Reader r; 
354            if(ibuf!=null) { r=ibuf; ibuf=null; r.close(); }
355         } catch(IOException e) 
356            { client.printerr(e); }
357      }
358      return fs;
359   }
360
361   public CoFile[] listCoFiles(CoFilenameFilter filter)
362      throws SecurityException
363   {  FtpFile[] fs = (FtpFile[])listCoFiles(); 
364      if(filter!=null)
365      {  Vector fv = new Vector();
366         for(int i=0;i<fs.length;i++)
367            if(filter.accept(this,fs[i].getName()))
368               fv.addElement(fs[i]);
369         fs = new FtpFile[fv.size()];
370         fv.copyInto(fs); }
371      return fs;
372   }
373
374   private static FtpFile examineListLine(FtpFile path,String line)
375      throws NoSuchElementException
376   {  if("0123456789".indexOf(line.charAt(0))<0)
377         /* unix list format never strarts with number */
378         return FtpFile.examineUnixListLine(path,line); 
379      /* windows list format always starts with number */
380      else return FtpFile.examineWinListLine(path,line); }
381
382   private static FtpFile examineNameListLine(FtpFile path,String line,int listtype)
383      throws NoSuchElementException
384   {  FtpFile ff = new FtpFile();
385      ff.client = path.client;
386      switch(listtype)
387      {
388      case FtpContext.NAME_LIST:
389         /**************************************
390         * file folder executable link special *
391         **************************************/
392         if(line.indexOf('.') != -1)
393            ff.access   = "-?????????";
394         else ff.access = "d?????????";
395         break;
396      case FtpContext.NAME_LIST_LS_P:
397         /***************************************
398         * file folder/ executable link special *
399         ***************************************/
400         if(line.endsWith("/"))
401         {  ff.access = "d?????????";
402            line = line.substring(0,line.length()-1);
403         } else ff.access   = "-?????????";
404         break;
405      case FtpContext.NAME_LIST_LS_F:
406         /*****************************************
407         * file folder/ executable* link@ special *
408         *****************************************/
409         if(line.endsWith("/"))
410         {  ff.access = "d?????????";
411            line = line.substring(0,line.length()-1);
412         } else 
413         if(line.endsWith("*"))
414         {  ff.access = "-??x??x??x";
415            line = line.substring(0,line.length()-1);
416         } else 
417         if(line.endsWith("@"))
418         {  ff.access = "l?????????";
419            line = line.substring(0,line.length()-1);
420         } else
421            ff.access = "-?????????";
422         break;
423      }
424      ff.owner       = "";
425      ff.group       = "";
426      ff.size        = -1; 
427      ff.date        = 0L;
428      ff.path        = path.getAbsolutePath();
429
430      if(!ff.path.endsWith("/"))
431         ff.path = ff.path + '/' + line;
432      else ff.path = ff.path + line;
433
434      ff.sortSetup(line);       
435
436      /* Skip current '.' and parent '..' directory aliases. */
437      if(ff.getName().compareTo(".")==0 || ff.getName().compareTo("..")==0)
438         throw new NoSuchElementException("skip");      
439      return ff;
440   }
441
442   private static FtpFile examineWinListLine(FtpFile path,String line) 
443      throws NoSuchElementException
444   {  FtpFile ff = new FtpFile();
445      ff.client = path.client;
446      /**********************************************
447      * 10-16-01  11:35PM                 1479 file *
448      * 10-16-01  11:37PM       <DIR>          awt  *
449      **********************************************/
450      try 
451      {  StringTokenizer toker = new StringTokenizer(line);
452         ff.date          = examineWinListDate
453                           (toker.nextToken(),  /* date */
454                            toker.nextToken()); /* time */
455         String size2dir  = toker.nextToken();  /* size or dir */
456         if(size2dir.equals("<DIR>"))
457         {  ff.access     = "d?????????";
458            ff.size       = -1; 
459         } else {
460            ff.access     = "-?????????";
461            ff.size       = Long.parseLong(size2dir); } 
462         String name      = toker.nextToken("").trim(); /* name */
463         
464         ff.owner         = "";
465         ff.group         = "";
466
467         ff.path = path.toString();  
468
469         if(!ff.path.endsWith("/"))
470            ff.path = ff.path + '/' + name;
471         else ff.path = ff.path + name;  
472         
473         ff.sortSetup(name);
474      } 
475      catch(NumberFormatException e)
476         { throw new NoSuchElementException("Win-List: Invalid Number Format"); }
477      /* Skip current '.' and parent '..' directory aliases. */
478      if(ff.getName().compareTo(".")==0 || ff.getName().compareTo("..")==0)
479         throw new NoSuchElementException("skip");
480      return ff;
481   }
482
483   private static long examineWinListDate(String date, String time)
484   {  /**********************
485      * 10-16-01    11:35PM *
486      * 10-16-2001  11:35PM *
487      **********************/
488      Calendar c = Calendar.getInstance();
489      try
490      {  StringTokenizer toker = new StringTokenizer(date,"-");
491         int m = Integer.parseInt(toker.nextToken()),
492             d = Integer.parseInt(toker.nextToken()),
493             y = Integer.parseInt(toker.nextToken());
494         if(y>=70) y+=1900; else y+=2000;
495         toker = new StringTokenizer(time,":APM");
496         c.set(y,m,d,(time.endsWith("PM")?12:0)+
497                     Integer.parseInt(toker.nextToken()),
498                     Integer.parseInt(toker.nextToken()));
499      }
500      catch(NumberFormatException e)
501         { throw new NoSuchElementException("Win-List: Invalid Date Format"); }
502      return c.getTime().getTime();
503   }
504
505   private static FtpFile examineUnixListLine(FtpFile path,String line) 
506      throws NoSuchElementException
507   {  FtpFile ff = new FtpFile();
508      ff.client = path.client;
509
510      /*********************************************************************
511      * -rw-r--r--   1 owner   group       239 Nov  9  1998 file           *
512      * crw-rw-rw-   1 root    sys      11, 42 Aug  3  2000 sun_device     * 
513      * crw-------   1 root    sys  137 0x0089 Nov 25 11:39 hpux_device    * 
514      * drw-r--r--   1 owner   group        58 Nov 12 13:51 folder         *
515      * lrw-r--r--   1 owner   group        58 Nov 12 13:51 link -> source *
516      * -rw-r--r--   1 4                    58 Nov 12 13:51 uu_file        *
517      * crw-------   1 4            137 0x0089 Nov 25 11:39 uu_device      * 
518      * drw-r--r--   1 4                    58 Nov 12 13:51 uu_folder      *
519      * lrw-r--r--   1 4                    58 Nov 12 13:51 uu_link -> src *
520      **********************************************************************/
521      try 
522      {  if(line.indexOf("->")>=0)
523            line = line.substring(0,line.indexOf("->"));
524         StringTokenizer toker = new StringTokenizer(line);
525         ff.access        = toker.nextToken();  /* access */
526                            toker.nextToken();  /* links */
527         ff.owner         = toker.nextToken();  /* owner */
528         ff.group         = toker.nextToken();  /* group */
529         String      size = toker.nextToken();  /* size */
530         if(size.endsWith(","))
531            size = size.substring(0,size.indexOf(","));
532         String uu = size;
533         if(ff.access.startsWith("c")) 
534            uu = toker.nextToken();             /* device */
535         /* if uu.charAt(0) is not digit try uu_file format */
536         if("0123456789".indexOf(uu.charAt(0))<0)
537            { size = ff.group; ff.group = ""; }
538         ff.size          = Integer.parseInt(size);
539         ff.date          = examineUnixListDate
540         (("0123456789".indexOf(uu.charAt(0))<0?uu
541                           :toker.nextToken()), /* month */
542                            toker.nextToken(),  /* day */
543                            toker.nextToken()); /* time or year */
544         String name      = toker.nextToken("").trim(); /* name */
545
546         ff.path = path.toString();  
547         if(!ff.path.endsWith("/"))
548            ff.path = ff.path + '/' + name;
549         else ff.path = ff.path + name;  
550         ff.sortSetup(name);
551      } 
552      catch(NumberFormatException e)
553         { throw new NoSuchElementException("Unix-List: Invalid Number Format"); }
554      catch(NoSuchElementException e)
555      {  /* Skip 'total n' message. */
556         try
557         {  StringTokenizer toker = new StringTokenizer(line);
558            if(!toker.nextToken().equals("total"))
559               throw e;
560            Long.parseLong(toker.nextToken());
561            if(!toker.hasMoreTokens())
562              throw new NoSuchElementException("skip");
563            else throw e;
564         } catch(NumberFormatException x)
565            { throw e; }
566      }
567      /* Skip current '.' and parent '..' directory aliases. */
568      if(ff.getName().compareTo(".")==0 || ff.getName().compareTo("..")==0)
569         throw new NoSuchElementException("skip");
570      return ff;
571   }
572   
573   private static long examineUnixListDate(String month, String day, String year2time)
574   {  /***************
575      * Nov  9  1998 *
576      * Nov 12 13:51 *
577      ***************/
578      Calendar c = Calendar.getInstance();
579      month = month.toUpperCase();
580      try
581      {  for(int m=0;m<12;m++)
582            if(month.equals(months[m]))
583            {  if(year2time.indexOf(':')!= -1)
584               {  /* current year */
585                  c.setTime(new Date(System.currentTimeMillis()));
586                  StringTokenizer toker 
587                     = new StringTokenizer(year2time,":");
588                  /* date and time */
589                  c.set(c.get(Calendar.YEAR), m, 
590                        Integer.parseInt(day), 
591                        Integer.parseInt(toker.nextToken()), 
592                        Integer.parseInt(toker.nextToken()));
593               } else 
594                  /* date */
595                  c.set(Integer.parseInt(year2time),m, 
596                        Integer.parseInt(day),0,0);
597               break; }
598      }
599      catch(NumberFormatException e)
600         { throw new NoSuchElementException("Unix-List: Invalid Date Format"); }
601      return c.getTime().getTime();
602   }
603
604   public String toString()   
605      { return getAbsolutePath(); }   
606}