ExpandIvy.java
001 /*
002  * Copyright (c) 1995-2012, The University of Sheffield. See the file
003  * COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
004  
005  * This file is part of GATE (see http://gate.ac.uk/), and is free software,
006  * licenced under the GNU Library General Public License, Version 2, June 1991
007  * (in the distribution as file licence.html, and also available at
008  * http://gate.ac.uk/gate/licence.html).
009  
010  * Mark A. Greenwood, 12/01/2012
011  
012  * $Id: ExpandIvy.java 17530 2014-03-04 15:57:43Z markagreenwood $
013  */
014 
015 package gate.util.ant;
016 
017 import gate.util.persistence.PersistenceManager;
018 
019 import java.io.File;
020 import java.io.FileWriter;
021 import java.io.IOException;
022 import java.net.URL;
023 import java.text.ParseException;
024 import java.util.List;
025 import java.util.Map;
026 import java.util.Set;
027 
028 import org.apache.ivy.Ivy;
029 import org.apache.ivy.core.LogOptions;
030 import org.apache.ivy.core.report.ArtifactDownloadReport;
031 import org.apache.ivy.core.report.ResolveReport;
032 import org.apache.ivy.core.resolve.ResolveOptions;
033 import org.apache.ivy.core.retrieve.RetrieveOptions;
034 import org.apache.ivy.core.settings.IvySettings;
035 import org.apache.ivy.util.DefaultMessageLogger;
036 import org.apache.ivy.util.Message;
037 import org.apache.ivy.util.filter.Filter;
038 import org.apache.ivy.util.filter.FilterHelper;
039 import org.apache.tools.ant.BuildException;
040 import org.apache.tools.ant.Task;
041 import org.apache.tools.ant.taskdefs.Copy;
042 import org.jdom.Document;
043 import org.jdom.Element;
044 import org.jdom.JDOMException;
045 import org.jdom.input.SAXBuilder;
046 import org.jdom.output.Format;
047 import org.jdom.output.XMLOutputter;
048 import org.jdom.xpath.XPath;
049 
050 /**
051  * An ANT task that takes a CREOLE plugin and adds local copies of Ivy managed
052  * dependencies. This involves copying JAR files into the plugin directory as
053  * well as updating the creole.xml to substitute the IVY elements with
054  * appropriate JAR elements.
055  */
056 public class ExpandIvy extends Task {
057 
058   private XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
059 
060   private File dir, settings;
061 
062   private boolean verbose = false;
063 
064   private boolean fully = false;
065 
066   /**
067    * Get the CREOLE plugin directory being processed.
068    
069    @return the CREOLE plugin directory being processed.
070    */
071   public File getDir() {
072     return dir;
073   }
074 
075   /**
076    * Set the CREOLE plugin directory to be processed.
077    
078    @param dir
079    *          the CREOLE plugin directory to be processed.
080    */
081   public void setDir(File dir) {
082     this.dir = dir;
083   }
084 
085   /**
086    * Get the Ivy settings file used to control dependency resolution.
087    
088    @return the Ivy settings file used to control dependency resolution, or
089    *         null if the default settings are being used.
090    */
091   public File getSettings() {
092     return dir;
093   }
094 
095   /**
096    * Specifies the settings file used to control dependency resolution.
097    
098    @param settings
099    *          the settings file used to control dependency resolution, or null
100    *          to use the default settings.
101    */
102   public void setSettings(File settings) {
103     this.settings = settings;
104   }
105 
106   /**
107    * If true then Ivy will spit out lots of messages while resolving
108    * dependencies.
109    
110    @return if true then Ivy will spit out lots of messages while resolving
111    *         dependencies.
112    */
113   public boolean getVerbose() {
114     return verbose;
115   }
116 
117   /**
118    * Controls the log level of Ivy.
119    
120    @param verbose
121    *          if true then Ivy will spit out lots of messages while resolving
122    *          dependencies.
123    */
124   public void setVerbose(boolean verbose) {
125     this.verbose = verbose;
126   }
127 
128   /**
129    * Should we fully remove the link to Ivy by removing the dependency XML
130    * files.
131    
132    @return if true Ivy files referenced in creole.xml will be removed after
133    *         they have been processed.
134    */
135   public boolean getFully() {
136     return fully;
137   }
138 
139   /**
140    * If true Ivy files referenced in creole.xml will be removed after they have
141    * been processed.
142    
143    @param fully
144    *          if true Ivy files referenced in creole.xml will be removed after
145    *          they have been processed.
146    */
147   public void setFully(boolean fully) {
148     this.fully = fully;
149   }
150 
151   @Override
152   public void execute() throws BuildException {
153     if(dir == null)
154       throw new BuildException("Please specify a directory", getLocation());
155 
156     if(!dir.exists() || !dir.isDirectory())
157       throw new BuildException("Specified directory doesn't exist",
158           getLocation());
159 
160     File creoleXml = new File(dir, "creole.xml");
161 
162     if(!creoleXml.exists())
163       throw new BuildException("Supplied directory isn't a CREOLE plugin");
164 
165     try {
166       // load the creole.xml into a JDOM structure
167       SAXBuilder builder = new SAXBuilder();
168       Document creoleDoc = builder.build(creoleXml);
169 
170       // get the IVY elements from the creole XML file
171       List<Element> ivyElts = getIvyElements(creoleDoc);
172 
173       if(ivyElts.size() 0) {
174         // if there are some elements to process then we have work to do...
175 
176         // get a configured Ivy instance
177         Ivy ivy =
178             getIvy(settings != null
179                 ? settings.toURI().toURL()
180                 : getSettingsURL(), dir);
181 
182         // we only want the binary jars so create a filter for them
183         Filter filter = FilterHelper.getArtifactTypeFilter(new String[]{"jar"});
184 
185         // set up the options for doing a resolve
186         ResolveOptions resolveOptions = new ResolveOptions();
187         resolveOptions.setArtifactFilter(filter);
188         if(!verboseresolveOptions.setLog(LogOptions.LOG_QUIET);
189 
190         // set up the options for doing a retrieve
191         RetrieveOptions retrieveOptions = new RetrieveOptions();
192         retrieveOptions.setArtifactFilter(filter);
193         if(!verboseretrieveOptions.setLog(LogOptions.LOG_QUIET);
194 
195         // an ANT task to handle all the copying
196         Copy copyTask;
197 
198         for(Element e : ivyElts) {
199           // for each IVY element in the creole.xml file....
200 
201           // get the location of the ivy file (assume ivy.xml if not specified)
202           File ivyFile = getIvyFile(e, creoleXml);
203 
204           if(!ivyFile.exists())
205             throw new BuildException("Referenced ivy file does not exist: "
206                 + ivyFile, getLocation());
207 
208           // remove the IVY element from the XML
209           Element parent = e.getParentElement();
210           parent.removeContent(e);
211 
212           // get ivy to resolve the dependencies and generate a report
213           ResolveReport report =
214               ivy.resolve(ivyFile.toURI().toURL(), resolveOptions);
215 
216           if(report.getAllProblemMessages().size() 0)
217             throw new BuildException("Unable to resolve all IVY dependencies",
218                 getLocation());
219 
220           // don't do a retrieve but find out what it would actually do.
221           // NOTE: we need to do this as a retrieve just returns the number of
222           // jar files copied and not what they were which we need to update the
223           // creole.xml file
224           @SuppressWarnings("unchecked")
225           Map<ArtifactDownloadReport, Set<String>> toCopy =
226               ivy.getRetrieveEngine().determineArtifactsToCopy(
227                   report.getModuleDescriptor().getModuleRevisionId(),
228                   ivy.getSettings().substitute(
229                       ivy.getSettings().getVariable("ivy.retrieve.pattern")),
230                   retrieveOptions);
231 
232           for(Map.Entry<ArtifactDownloadReport, Set<String>> entry : toCopy
233               .entrySet()) {
234             // for each artifact a retrieve would copy....
235             ArtifactDownloadReport dlReport =
236                 entry.getKey();
237 
238             for(String destPath : entry.getValue()) {
239               // find out where it should end up
240               File destFile = new File(destPath);
241 
242               // make sure the dir actually exists
243               destFile.getParentFile().mkdirs();
244 
245               // set up ANT ready to copy from the cache into the plugin dir
246               copyTask = new Copy();
247               copyTask.setProject(getProject());
248               copyTask.setLocation(getLocation());
249               copyTask.setTaskName(getTaskName());
250               copyTask.setFile(dlReport.getLocalFile());
251               copyTask.setTofile(destFile);
252               copyTask.init();
253 
254               // do the actual copy
255               copyTask.perform();
256 
257               // add a new JAR element to creole.xml pointing at the newly added
258               // jar
259               Element jarElement =
260                   new Element("JAR").setText(PersistenceManager
261                       .getRelativePath(dir.toURI().toURL(), destFile.toURI()
262                           .toURL()));
263               parent.addContent(jarElement);
264             }
265 
266             if(fully && !ivyFile.delete()) ivyFile.deleteOnExit();
267           }
268         }
269 
270         // now we have finished write the new XML back to creole.xml
271         outputter.output(creoleDoc, new FileWriter(creoleXml));
272       }
273     catch(Exception e) {
274       // if anything goes wrong just re-throw the exception
275       throw new BuildException(e);
276     }
277   }
278 
279   /**
280    * Processes the specified creole.xml file to extract all the &lt;IVY&gt;
281    * elements
282    
283    @param creoleXML
284    *          the URL of the creole.xml file to process
285    @return a list of the &lt;IVY&gt; XML elements
286    */
287   public static List<Element> getIvyElements(URL creoleXML)
288       throws JDOMException, IOException {
289     // load the creole.xml into a JDOM structure
290     SAXBuilder builder = new SAXBuilder();
291     Document doc = builder.build(creoleXML);
292     return getIvyElements(doc);
293   }
294 
295   /**
296    * Processes the specified XML document file to extract all the &lt;IVY&gt;
297    * elements
298    
299    @param doc
300    *          the XML document to process
301    @return a list of the &lt;IVY&gt; XML elements
302    */
303   @SuppressWarnings("unchecked")
304   public static List<Element> getIvyElements(Document docthrows JDOMException {
305     // use XPath to find all the IVY elements
306     XPath jarXPath =
307         XPath.newInstance("//*[translate(local-name(), 'ivy', 'IVY') = 'IVY']");
308     return jarXPath.selectNodes(doc);
309   }
310 
311   /**
312    * Turns an &lt;IVY&gt; XML element into a File instance by resolving relative
313    * to the creole.xml file.
314    
315    @param element
316    *          the &lt;IVY&gt; element to convert
317    @param creoleXML
318    *          the creole.xml file to resolve relative to
319    @return a File instance pointing to the Ivy file specified by the XML
320    *         element
321    */
322   public static File getIvyFile(Element element, File creoleXML) {
323     return new File(creoleXML.getParentFile(), getIvyPath(element));
324   }
325 
326   /**
327    * Retrieve the path to the Ivy file as specified in the XML element. If no
328    * path is given use the default of 'ivy.xml'.
329    
330    @param element
331    *          the &lt;IVY&gt; XML element to process
332    @return the path to the Ivy file as specified in the XML element, defaults
333    *         to 'ivy.xml'.
334    */
335   public static String getIvyPath(Element element) {
336     String ivyText = element.getTextTrim();
337     if(ivyText == null || ivyText.equals("")) ivyText = "ivy.xml";
338     return ivyText;
339   }
340 
341   public static Ivy getIvy() throws ParseException, IOException {
342     return getIvy(null, null);
343   }
344 
345   public static Ivy getIvy(File dirthrows ParseException, IOException {
346     return getIvy(null, dir);
347   }
348 
349   public static Ivy getIvy(URL settingsthrows ParseException, IOException {
350     return getIvy(settings, null);
351   }
352 
353   public static Ivy getIvy(URL settings, File dirthrows ParseException,
354       IOException {
355     IvySettings ivySettings = new IvySettings();
356 
357     if(settings != null)
358       ivySettings.load(settings);
359     else ivySettings.loadDefault();
360 
361     if(dir != nullivySettings.setBaseDir(dir);
362 
363     // get an instance of ivy
364     return Ivy.newInstance(ivySettings);
365   }
366 
367   /**
368    * Attempts to find a custom Ivy settings file to use instead of the default
369    * configuration. This looks first for a system property
370    <code>ivy.settings.file</code> and then <code>ivy.settings.url</code>. If
371    * neither exist or can be converted to a valid URL then the method returns
372    * null.
373    
374    @return the URL of the settings file to use or null if one was not
375    *         specified or could not be correctly converted.
376    */
377   public static URL getSettingsURL() {
378 
379     String val = System.getProperty("ivy.settings.file");
380     if(val != null) {
381       try {
382         File file = new File(val);
383         if(file.exists() && file.isFile() && file.canRead())
384           return file.toURI().toURL();
385       catch(Exception e) {
386         // ignore this and try the URL
387         System.err.println("Ivalid ivy.settings.file will be ignored: " + val);
388       }
389     }
390 
391     val = System.getProperty("ivy.settings.url");
392     if(val != null) {
393       try {
394         return new URL(val);
395       catch(Exception e) {
396         // ignore this
397         System.err.println("Ivalid ivy.settings.url will be ignored: " + val);
398       }
399     }
400 
401     // neither of the system properties were helpful so return null
402     return null;
403   }
404 
405   static {
406     // this seems to be the only way to suppress the loading settings message
407     Message.setDefaultLogger(new DefaultMessageLogger(Message.MSG_ERR));
408   }
409 }