GappModel.java
001 package gate.util.ant.packager;
002 
003 import gate.util.Files;
004 import gate.util.GateRuntimeException;
005 import gate.util.LuckyException;
006 import gate.util.persistence.PersistenceManager;
007 
008 import java.io.BufferedOutputStream;
009 import java.io.File;
010 import java.io.FileOutputStream;
011 import java.io.IOException;
012 import java.net.MalformedURLException;
013 import java.net.URL;
014 import java.util.ArrayList;
015 import java.util.HashMap;
016 import java.util.List;
017 import java.util.Map;
018 import java.util.Set;
019 
020 import org.jdom.Document;
021 import org.jdom.Element;
022 import org.jdom.JDOMException;
023 import org.jdom.input.SAXBuilder;
024 import org.jdom.output.Format;
025 import org.jdom.output.XMLOutputter;
026 import org.jdom.xpath.XPath;
027 
028 public class GappModel {
029   private Document gappDocument;
030 
031   /**
032    * The URL at which this GAPP file is saved.
033    */
034   private URL gappFileURL;
035 
036   /**
037    * The URL against which to resolve $gatehome$ relative paths.
038    */
039   private URL gateHomeURL;
040 
041   /**
042    * The URL against which to resolve $resourceshome$ relative paths.
043    */
044   private URL resourcesHomeURL;
045 
046   /**
047    * Map whose keys are the resolved URLs of plugins referred to by relative
048    * paths in the GAPP file and whose values are the JDOM Elements of the
049    * <urlString> elements concerned.
050    */
051   private Map<URL, List<Element>> pluginRelpathsMap =
052           new HashMap<URL, List<Element>>();
053 
054   /**
055    * Map whose keys are the resolved URLs of resource files other than plugin
056    * directories referred to by relative paths in the GAPP file and whose
057    * values are the JDOM Elements of the &lt;urlString&gt; elements concerned.
058    */
059   private Map<URL, List<Element>> resourceRelpathsMap =
060           new HashMap<URL, List<Element>>();
061 
062   /**
063    * XPath selecting all urlStrings that contain $relpath$ or $gatehome$ in the
064    * &lt;application&gt; section of the file.
065    */
066   private static XPath relativeResourcePathElementsXPath;
067 
068   /**
069    * XPath selecting all urlStrings that contain $relpath$ or $gatehome$ in the
070    * &lt;urlList&gt; section of the file.
071    */
072   private static XPath relativePluginPathElementsXPath;
073 
074   /**
075    @see #GappModel(URL,URL, URL)
076    */
077   public GappModel(URL gappFileURL) {
078     this(gappFileURL, null, null);
079   }
080 
081   /**
082    @see #GappModel(URL,URL, URL)
083    */
084   public GappModel(URL gappFileURL, URL gateHomeURL) {
085     this(gappFileURL, gateHomeURL, null);
086   }
087 
088   /**
089    * Create a GappModel for a GAPP file.
090    
091    @param gappFileURL the URL of the GAPP file to model.
092    @param gateHomeURL the URL against which $gatehome$ relative paths should
093    * be resolved.  This may be null if you are sure that the GAPP you are
094    * packaging does not contain any $gatehome$ paths.  If no gateHomeURL is
095    * provided but the application does contain a $gatehome$ path, a
096    * GateRuntimeException will be thrown.
097    @param resourcesHomeURL the URL against which $resourceshome$ relative paths should
098    * be resolved.  This may be null if you are sure that the GAPP you are
099    * packaging does not contain any $resourceshome$ paths.  If no gateHomeURL is
100    * provided but the application does contain a $resourceshome$ path, a
101    * GateRuntimeException will be thrown.
102    */
103   @SuppressWarnings("unchecked")
104   public GappModel(URL gappFileURL, URL gateHomeURL, URL resourcesHomeURL) {
105     if(!"file".equals(gappFileURL.getProtocol())) {
106       throw new GateRuntimeException("GAPP URL must be a file: URL");
107     }
108     if(gateHomeURL != null && !"file".equals(gateHomeURL.getProtocol())) {
109       throw new GateRuntimeException("GATE home URL must be a file: URL");
110     }
111     this.gappFileURL = gappFileURL;
112     this.gateHomeURL = gateHomeURL;
113     this.resourcesHomeURL = resourcesHomeURL;
114 
115     try {
116       SAXBuilder builder = new SAXBuilder();
117       this.gappDocument = builder.build(gappFileURL);
118     }
119     catch(Exception ex) {
120       throw new GateRuntimeException("Error parsing GAPP file", ex);
121     }
122 
123     // compile the XPath expression
124     if(relativeResourcePathElementsXPath == null) {
125       try {
126         relativeResourcePathElementsXPath =
127                 XPath
128                         .newInstance(
129                                 // URLHolder elements as map entry values
130                                   "/gate.util.persistence.GateApplication/application"
131                                 "//gate.util.persistence.PersistenceManager-URLHolder"
132                                 "/urlString[starts-with(., '$relpath$') "
133                                 "or starts-with(., '$resourceshome$') "
134                                 "or starts-with(., '$gatehome$')]"
135                                 " | "
136                                 // specific Persistence object fields of type URLHolder
137                                 // (e.g. datastore location)
138                                 "/gate.util.persistence.GateApplication/application"
139                                 "//*[@class='gate.util.persistence.PersistenceManager$URLHolder']"
140                                 "/urlString[starts-with(., '$relpath$') "
141                                 "or starts-with(., '$resourceshome$') "
142                                 "or starts-with(., '$gatehome$')]");
143         relativePluginPathElementsXPath =
144                 XPath
145                         .newInstance("/gate.util.persistence.GateApplication/urlList"
146                                 "//gate.util.persistence.PersistenceManager-URLHolder"
147                                 "/urlString[starts-with(., '$relpath$') "
148                                 "or starts-with(., '$resourceshome$') "
149                                 "or starts-with(., '$gatehome$')]");
150       }
151       catch(JDOMException jdx) {
152         throw new GateRuntimeException("Error creating XPath expression", jdx);
153       }
154     }
155 
156     List<Element> resourceRelpaths = null;
157     List<Element> pluginRelpaths = null;
158     try {
159       // the compiler thinks this is unsafe, but we know that the XPath
160       // expression will only select Elements so it's OK really
161       resourceRelpaths =
162               relativeResourcePathElementsXPath.selectNodes(gappDocument);
163 
164       pluginRelpaths =
165               relativePluginPathElementsXPath.selectNodes(gappDocument);
166     }
167     catch(JDOMException e) {
168       throw new GateRuntimeException(
169               "Error extracting 'relpath' URLs from GAPP file", e);
170     }
171 
172     try {
173       buildRelpathsMap(resourceRelpaths, resourceRelpathsMap);
174       buildRelpathsMap(pluginRelpaths, pluginRelpathsMap);
175     }
176     catch(MalformedURLException mue) {
177       throw new GateRuntimeException(
178               "Error parsing relative paths in GAPP file", mue);
179     }
180   }
181 
182   private void buildRelpathsMap(List<Element> relpathElements,
183           Map<URL, List<Element>> relpathsMapthrows MalformedURLException {
184     for(Element el : relpathElements) {
185       String elementText = el.getText();
186       URL targetURL = null;
187       if(elementText.startsWith("$gatehome$")) {
188         // complain if gateHomeURL not set
189         if(gateHomeURL == null) {
190           throw new GateRuntimeException("Found a $gatehome$ relative path in "
191               "GAPP file, but no GATE home URL provided to resolve against");
192         }
193         String relativePath = el.getText().substring("$gatehome$".length());
194         targetURL = new URL(gateHomeURL, relativePath);
195       }
196       else if(elementText.startsWith("$resourceshome$")) {
197         // complain if gateHomeURL not set
198         if(gateHomeURL == null) {
199           throw new GateRuntimeException("Found a $resourceshome$ relative path in "
200               "GAPP file, but no resources home URL provided to resolve against");
201         }
202         String relativePath = el.getText().substring("$resourceshome$".length());
203         targetURL = new URL(resourcesHomeURL, relativePath);
204       }
205       else if(elementText.startsWith("$relpath$")) {
206         String relativePath = el.getText().substring("$relpath$".length());
207         targetURL = new URL(gappFileURL, relativePath);
208       }
209       List<Element> eltsForURL = relpathsMap.get(targetURL);
210       if(eltsForURL == null) {
211         eltsForURL = new ArrayList<Element>();
212         relpathsMap.put(targetURL, eltsForURL);
213       }
214       eltsForURL.add(el);
215     }
216   }
217 
218   /**
219    * Get the URL at which the GAPP file resides.
220    
221    @return the gappFileURL
222    */
223   public URL getGappFileURL() {
224     return gappFileURL;
225   }
226 
227   /**
228    * Set the URL at which the GAPP file resides. When this GappModel is
229    * constructed this will be the URL from which the file is loaded, but
230    * this should be changed if you wish to write the updated GAPP to
231    * another location.
232    
233    @param gappFileURL the gappFileURL to set
234    */
235   public void setGappFileURL(URL gappFileURL) {
236     this.gappFileURL = gappFileURL;
237   }
238 
239   /**
240    * Get the JDOM Document representing this GAPP file.
241    
242    @return the document
243    */
244   public Document getGappDocument() {
245     return gappDocument;
246   }
247 
248   /**
249    * Get the plugin URLs that are referenced by relative paths in this
250    * GAPP file.
251    
252    @return the set of URLs.
253    */
254   public Set<URL> getPluginURLs() {
255     return pluginRelpathsMap.keySet();
256   }
257 
258   /**
259    * Get the resource URLs that are referenced by relative paths in this
260    * GAPP file.
261    
262    @return the set of URLs.
263    */
264   public Set<URL> getResourceURLs() {
265     return resourceRelpathsMap.keySet();
266   }
267 
268   /**
269    * Update the modelled content of the GAPP file to replace any
270    * relative paths referring to <code>originalURL</code> with those
271    * pointing to <code>newURL</code>. If makeRelative is
272    <code>true</code>, the new path will be relativized against the
273    <b>current</b> {@link #gappFileURL}, so you should call
274    {@link #setGappFileURL} with the URL at which the file will
275    * ultimately be saved before calling this method. If
276    <code>makeRelative</code> is <code>false</code> the new URL
277    * will be used directly as an absolute URL (so to replace a relative
278    * path with the absolute URL to the same file you can call
279    <code>updatePathForURL(u, u, false)</code>).
280    
281    @param originalURL The original URL whose references are to be
282    *          replaced.
283    @param newURL the replacement URL.
284    @param makeRelative should we relativize the newURL before use?
285    */
286   public void updatePathForURL(URL originalURL, URL newURL, boolean makeRelative) {
287     List<Element> resourceEltsToUpdate = resourceRelpathsMap.get(originalURL);
288     List<Element> pluginEltsToUpdate = pluginRelpathsMap.get(originalURL);
289     if(resourceEltsToUpdate == null && pluginEltsToUpdate == null) {
290       return;
291     }
292 
293     String newPath;
294     if(makeRelative) {
295       newPath =
296               "$relpath$"
297                       + PersistenceManager.getRelativePath(gappFileURL, newURL);
298     }
299     else {
300       newPath = newURL.toExternalForm();
301     }
302 
303     if(resourceEltsToUpdate != null) {
304       for(Element e : resourceEltsToUpdate) {
305         e.setText(newPath);
306       }
307     }
308     if(pluginEltsToUpdate != null) {
309       for(Element e : pluginEltsToUpdate) {
310         e.setText(newPath);
311       }
312     }
313   }
314 
315   /**
316    * Finish up processing of the gapp file ready for writing.
317    */
318   @SuppressWarnings("unchecked")
319   public void finish() {
320     // remove duplicate plugin entries
321     try {
322       // this XPath selects all URLHolders out of the URL list that have
323       // the same URL string as one of their preceding siblings, i.e. if
324       // there are N URLs in the list with the same value then this
325       // XPath
326       // will select all but the first one of them.
327       XPath duplicatePluginXPath =
328               XPath
329                       .newInstance("/gate.util.persistence.GateApplication/urlList"
330                               "/localList/gate.util.persistence.PersistenceManager-URLHolder"
331                               "[urlString = preceding-sibling::gate.util.persistence.PersistenceManager-URLHolder/urlString]");
332       List<Element> duplicatePlugins =
333               duplicatePluginXPath.selectNodes(gappDocument);
334       for(Element e : duplicatePlugins) {
335         e.getParentElement().removeContent(e);
336       }
337     }
338     catch(JDOMException e) {
339       throw new LuckyException(
340               "Error applying XPath expression to remove duplicate plugins", e);
341     }
342   }
343 
344   /**
345    * Write out the (possibly modified) GAPP file to its new location.
346    
347    @throws IOException if an I/O error occurs.
348    */
349   public void write() throws IOException {
350     finish();
351     File newGappFile = Files.fileFromURL(gappFileURL);
352     FileOutputStream fos = new FileOutputStream(newGappFile);
353     BufferedOutputStream out = new BufferedOutputStream(fos);
354 
355     XMLOutputter outputter = new XMLOutputter(Format.getRawFormat());
356     outputter.output(gappDocument, out);
357   }
358 }