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