001package com.hfg.anttask;
002
003import java.io.File;
004import java.io.IOException;
005import java.util.Calendar;
006import java.util.Collections;
007import java.util.Iterator;
008import java.util.List;
009import java.util.regex.Matcher;
010import java.util.regex.Pattern;
011
012import org.apache.tools.ant.BuildException;
013import org.apache.tools.ant.Task;
014
015import com.hfg.datetime.DateSortDirection;
016import com.hfg.util.StringUtil;
017import com.hfg.util.io.HTTPRemoteFileLister;
018import com.hfg.util.io.RemoteFile;
019import com.hfg.util.io.RemoteFileLister;
020import com.hfg.util.io.RemoteFileListerFactory;
021import com.hfg.util.io.RemoteFileTimestampComparator;
022
023
024//------------------------------------------------------------------------------
025/**
026  Ant task for retrieving a particular file from a wildcarded URL source.
027  In the case of multiple matches, the newest is the one returned. HTTP, FTP and file system URL's are supported.
028  <p>
029  Normally, this task is used with files whose names contain a variable portion, the
030  matching of which allows '*' wildcarding.
031  </p>
032  <p>
033  Requires the jakarta commons-net jar (for FTP).
034  </p>
035  If the task's name is set and multiple matching is not enabled, five project properties are set:
036  <ul>
037  <li>Property <code>&lt;taskname&gt;</code> is set to the destination file's full path.</li>
038  <li>Property <code>&lt;taskname&gt;.name</code> is set to the destination file's name.</li>
039  <li>Property <code>&lt;taskname&gt;.basename</code> is set to the destination file's name minus the last '.extenstion'.</li>
040  <li>Property <code>&lt;taskname&gt;.baseURL</code> is set to the URL of the source file's parent directory.</li>
041  <li>Property <code>&lt;taskname&gt;.wildcard</code> is set to the portion of the URL corresponding to the first '*'.</li>
042  </ul>
043
044  <h3>Parameters</h3>
045  <table border="1" cellpadding="2" cellspacing="0">
046   <caption>Parameter descriptions</caption>
047   <tr>
048     <td valign="top"><b>Attribute</b></td>
049     <td valign="top"><b>Description</b></td>
050     <td align="center" valign="top"><b>Required</b></td>
051   </tr>
052   <tr>
053     <td valign="top">name</td>
054     <td valign="top">The name (handle) of the property.</td>
055     <td valign="top" align="center">No</td>
056   </tr>
057   <tr>
058     <td valign="top">src</td>
059     <td valign="top">The URL (http, ftp, or a file system path) of the remote file (may contain one or more '*' wildcards).</td>
060     <td valign="top" align="center">Yes</td>
061   </tr>
062   <tr>
063     <td valign="top">dest</td>
064     <td valign="top">The local file to copy into.</td>
065     <td valign="top" align="center" rowspan='2'>Yes</td>
066   </tr>
067   <tr>
068     <td valign="top">destdir</td>
069     <td valign="top">The local directory to copy into.
070                      The file's remote name is maintained.</td>
071   </tr>
072   <tr>
073     <td valign="top">matchmultiple</td>
074     <td valign="top">Used with destdir, retrieves all matching files and no properties are set.
075     If false, only the most recent matching file is retrieved and properties are set.
076     (false by default)</td>
077     <td valign="top" align="center">No</td>
078   </tr>
079   <tr>
080     <td valign="top">ignoreerrors</td>
081     <td valign="top">Do not halt ant execution on failure. (false by default)</td>
082     <td valign="top" align="center">No</td>
083   </tr>
084   <tr>
085     <td valign="top">usetimestamp</td>
086     <td valign="top">Use the file's timestamp and size to determine if freshening the
087     local copy is necessary. (true by default)</td>
088     <td valign="top" align="center">No</td>
089   </tr>
090   <tr>
091     <td valign="top">if</td>
092     <td valign="top">Only execute if a property of the given name exists in the current project.</td>
093     <td valign="top" align="center">No</td>
094   </tr>
095   <tr>
096     <td valign="top">unless</td>
097     <td valign="top">Only execute if a property of the given name doesn't exist in the current project.</td>
098     <td valign="top" align="center">No</td>
099   </tr>
100   <tr>
101     <td valign="top">useragent</td>
102     <td valign="top">User-Agent HTTP header to send, Ant will specify a User-Agent header of "Apache Ant VERSION" unless overridden by this attribute.</td>
103     <td valign="top" align="center">No</td>
104   </tr>
105  </table>
106  <p>
107   The <code>if</code> and <code>unless</code> attributes make the
108  execution conditional -both probe for the named property being defined.
109  The <code>if</code> tests for the property being defined, the
110  <code>unless</code> for a property being undefined.
111  </p>
112  If both attributes are set, then the task executes only if both tests
113  are true. i.e.
114  <pre>execute := defined(ifProperty) &amp;&amp; !defined(unlessProperty)</pre>
115
116  The following example downloads the most recent file matching the src pattern
117  to the specified local destination:<pre>
118      &lt;getwild name='ij'
119         src='http://rsb.info.nih.gov/ij/download/zips/ij*.zip'
120         dest='${tmp.dir}/ij.zip' /&gt;
121   </pre>
122  @author J. Alex Taylor, hairyfatguy.com
123 */
124//------------------------------------------------------------------------------
125// com.hfg XML/HTML Coding Library
126//
127// This library is free software; you can redistribute it and/or
128// modify it under the terms of the GNU Lesser General Public
129// License as published by the Free Software Foundation; either
130// version 2.1 of the License, or (at your option) any later version.
131//
132// This library is distributed in the hope that it will be useful,
133// but WITHOUT ANY WARRANTY; without even the implied warranty of
134// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
135// Lesser General Public License for more details.
136//
137// You should have received a copy of the GNU Lesser General Public
138// License along with this library; if not, write to the Free Software
139// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
140//
141// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
142// jataylor@hairyfatguy.com
143//------------------------------------------------------------------------------
144
145// Wanted to just extend org.apache.tools.ant.taskdefs.Get but I didn't have
146// access to the variables.
147// - JAT
148
149public class GetWild extends Task
150{
151   private String  mName;
152   private String  mSource; // required
153   private File mDest;
154   private File    mDestDir;
155   private boolean mUseTimestamp = true; //on by default
156   private boolean mIgnoreErrors = false;
157   private boolean mMatchMultiple = false;
158   private String  mIfCondition;
159   private String  mUnlessCondition;
160   private String  mUserAgentString;
161
162   //---------------------------------------------------------------------------
163   /**
164    * Set the task's name.
165    *
166    * @param name name for the task
167    */
168   public void setName(String name)
169   {
170      this.mName = name;
171   }
172
173   //---------------------------------------------------------------------------
174   /**
175    * The remote file for retrieval.
176    * @param inURL the remote file for retrieval
177    */
178   public void setSrc(String inURL)
179   {
180      this.mSource = inURL;
181   }
182
183   //---------------------------------------------------------------------------
184   /**
185    * Specifies where to copy the source file.
186    * @param inValue the local file to copy the remote file to
187    */
188   public void setDest(File inValue)
189   {
190      this.mDest = inValue;
191   }
192
193   //---------------------------------------------------------------------------
194   /**
195    * Specifies the local directory to copy the remote file into
196    * (preserving the file's remote name).
197    * @param inValue the local directory to copy the remote file into
198    */
199   public void setDestdir(File inValue)
200   {
201      this.mDestDir = inValue;
202   }
203
204
205   //---------------------------------------------------------------------------
206   /**
207    * Only execute if a property of the given name exists in the current project.
208    * @param inPropertyName property name
209    */
210   public void setIf(String inPropertyName)
211   {
212       mIfCondition = inPropertyName;
213   }
214
215   //---------------------------------------------------------------------------
216   /**
217    * Only execute if a property of the given name does not exist in the current project.
218    * @param inPropertyName property name
219    */
220   public void setUnless(String inPropertyName)
221   {
222       mUnlessCondition = inPropertyName;
223   }
224
225   //---------------------------------------------------------------------------
226   /**
227    * Specifies whether or not to retrieve multiple matching files.
228    * @param inValue whether or not to retrieve multiple matching files
229    */
230   public void setMatchMultiple(boolean inValue)
231   {
232      mMatchMultiple = inValue;
233   }
234
235   //---------------------------------------------------------------------------
236   /**
237    * Specifies whether or not to ignore retrieval errors.
238    * @param inValue whether or not to ignore retrieval errors
239    */
240   public void setIgnoreErrors(boolean inValue)
241   {
242      mIgnoreErrors = inValue;
243   }
244
245   //---------------------------------------------------------------------------
246   /**
247    * If set to true, the remote file's timestamp will be compared
248    * to the local file (if present) before downloading.
249    * @param inValue whether or not to compare the remote timestamp to the local file
250    */
251   public void setUseTimestamp(boolean inValue)
252   {
253      mUseTimestamp = inValue;
254   }
255
256   //---------------------------------------------------------------------------
257   /**
258    * User-Agent HTTP header to send, Ant will specify a User-Agent header of "Apache Ant VERSION" unless overridden by this attribute.
259    * @param inValue the value to use for the User-Agent header field in HTTP requests
260    */
261   public void setUserAgent(String inValue)
262   {
263      mUserAgentString = inValue;
264   }
265
266   //---------------------------------------------------------------------------
267   /**
268    * Does the work.
269    *
270    * @exception BuildException Thrown in unrecoverable error.
271    */
272   public void execute()
273         throws BuildException
274   {
275      if (TaskUtil.testIfCondition(mIfCondition, getProject())
276          && TaskUtil.testUnlessCondition(mUnlessCondition, getProject()))
277      {
278         if (mSource == null)
279         {
280            throw new BuildException("'src' attribute is required", getLocation());
281         }
282
283         if (mSource.startsWith("http"))
284         {
285            String antVersionString = getProject().getProperty("ant.version");
286            mUserAgentString = "Apache Ant" + (StringUtil.isSet(antVersionString) ? " " + antVersionString : "");
287         }
288
289         List remoteFiles = getRemoteFiles();
290         File destination = getDestination();
291
292         if (mMatchMultiple)
293         {
294            if (null == mDestDir)
295            {
296               throw new BuildException("'destdir' attribute is required when matchmultiple is true",
297                                        getLocation());
298            }
299
300            Iterator iter = remoteFiles.iterator();
301            while (iter.hasNext())
302            {
303               RemoteFile remoteFile = (RemoteFile) iter.next();
304               retrieveRemoteFile(remoteFile, destination);
305            }
306         }
307         else
308         {
309            retrieveRemoteFile((RemoteFile) remoteFiles.get(0), destination);
310         }
311      }
312   }
313
314   //---------------------------------------------------------------------------
315   private File getDestination()
316   {
317      File destination = null;
318
319      if (mDest == null
320          && mDestDir == null)
321      {
322         throw new BuildException("Either a 'dest' or 'destdir' attribute is required",
323                                  getLocation());
324      }
325
326      if (mDest != null)
327      {
328         if (mDest.exists() && mDest.isDirectory())
329         {
330            throw new BuildException("The specified destination is a directory",
331                                     getLocation());
332         }
333
334         if (mDest.exists() && !mDest.canWrite())
335         {
336            throw new BuildException("Can't write to " + mDest.getAbsolutePath(),
337                                     getLocation());
338         }
339
340         destination = mDest;
341      }
342      else if (mDestDir != null)
343      {
344         if (mDestDir.exists() && !mDestDir.isDirectory())
345         {
346            throw new BuildException("The specified destdir is not a directory",
347                                     getLocation());
348         }
349
350         if (!mDestDir.exists())
351         {
352            mDestDir.mkdirs();
353         }
354
355         destination = mDestDir;
356      }
357
358      return destination;
359   }
360
361   //---------------------------------------------------------------------------
362   private List getRemoteFiles()
363   {
364      RemoteFileLister lister = RemoteFileListerFactory.getRemoteFileLister(mSource);
365      if (lister instanceof HTTPRemoteFileLister)
366      {
367         ((HTTPRemoteFileLister) lister).setUserAgentString(mUserAgentString);
368      }
369
370      List remoteFiles = lister.getUnfilteredRemoteFileList();
371      if (0 == remoteFiles.size())
372      {
373         throw new BuildException( "No file found matching '" + mSource + "'");
374      }
375
376      Collections.sort(remoteFiles, new RemoteFileTimestampComparator(DateSortDirection.NEWER_TO_OLDER));
377
378      return remoteFiles;
379   }
380
381
382   //---------------------------------------------------------------------------
383   private void retrieveRemoteFile(RemoteFile inRemoteFile, File inLocalDest)
384   {
385      log("Getting: " + inRemoteFile.getPath());
386
387      Calendar timestamp = inRemoteFile.getTimestamp(); // Makes a HEAD request and will update the url for redirections
388
389      File destFile;
390      if (inLocalDest.isDirectory())
391      {
392         destFile = new File(inLocalDest, inRemoteFile.getName());
393      }
394      else
395      {
396         destFile = inLocalDest;
397      }
398      log("Local File: " + destFile);
399
400
401      if (mUseTimestamp
402            && timestamp != null
403            && destFile.exists()
404            && inRemoteFile.getTimestamp().getTimeInMillis() <= destFile.lastModified()
405            && inRemoteFile.getSize() == destFile.length())
406      {
407         // Already up-to-date.
408         log("Already up-to-date");
409      }
410      else
411      {
412         try
413         {
414            inRemoteFile.writeToLocalFile(destFile);
415         }
416         catch (IOException e)
417         {
418            log("Error getting " + mSource + " to " + destFile + ": " + e.toString());
419            if (!mIgnoreErrors) throw new BuildException(e, getLocation());
420         }
421      }
422
423      if (mName != null
424            && !mMatchMultiple)
425      {
426         setProjectProperties(inRemoteFile, destFile);
427      }
428   }
429
430   //---------------------------------------------------------------------------
431   private void setProjectProperties(RemoteFile inRemoteFile, File inDestinationFile)
432   {
433      getProject().setProperty(mName, inDestinationFile.getPath());
434      getProject().setProperty(mName + ".name", inDestinationFile.getName());
435
436
437      int dotIndex = inDestinationFile.getName().lastIndexOf(".");
438      String basename = (dotIndex > 0 ? inDestinationFile.getName().substring(0, dotIndex) :
439                                        inDestinationFile.getName());
440
441      getProject().setProperty(mName + ".basename", basename);
442
443
444      int slashIndex = inRemoteFile.getPath().lastIndexOf("/");
445      if (slashIndex >= 0)
446      {
447         getProject().setProperty(mName + ".baseURL", inRemoteFile.getURL().substring(0, slashIndex));
448      }
449
450      int wildcardIndex = mSource.indexOf("*");
451      if (wildcardIndex > 0)
452      {
453         int prevSlash = mSource.lastIndexOf("/", wildcardIndex);
454         int postSlash = mSource.indexOf("/", wildcardIndex);
455         if (postSlash < 0)
456         {
457            postSlash = mSource.length();
458         }
459
460         Matcher m = Pattern.compile(mSource.substring(prevSlash, wildcardIndex) + "(.+)" + mSource.substring(wildcardIndex  + 1, postSlash)).matcher(inRemoteFile.getURL());
461         if (m.find())
462         {
463            getProject().setProperty(mName + ".wildcard", m.group(1));
464         }
465      }
466   }
467}