View Javadoc

1   /**
2    *
3    *  Licensed to the Apache Software Foundation (ASF) under one or more
4    *  contributor license agreements.  See the NOTICE file distributed with
5    *  this work for additional information regarding copyright ownership.
6    *  The ASF licenses this file to You under the Apache License, Version 2.0
7    *  (the "License"); you may not use this file except in compliance with
8    *  the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing, software
13   *  distributed under the License is distributed on an "AS IS" BASIS,
14   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   *  See the License for the specific language governing permissions and
16   *  limitations under the License.
17   */
18  package org.apache.geronimo.system.plugin;
19  
20  import java.io.File;
21  import java.io.FileNotFoundException;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.BufferedOutputStream;
26  import java.net.HttpURLConnection;
27  import java.net.MalformedURLException;
28  import java.net.URL;
29  import java.net.URLConnection;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Collection;
33  import java.util.Collections;
34  import java.util.HashMap;
35  import java.util.HashSet;
36  import java.util.Iterator;
37  import java.util.LinkedList;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Set;
41  import java.util.SortedSet;
42  import java.util.Enumeration;
43  import java.util.jar.JarEntry;
44  import java.util.jar.JarFile;
45  import java.util.jar.JarOutputStream;
46  import java.util.jar.Manifest;
47  import java.util.zip.ZipEntry;
48  import javax.security.auth.login.FailedLoginException;
49  import javax.xml.parsers.DocumentBuilder;
50  import javax.xml.parsers.DocumentBuilderFactory;
51  import javax.xml.parsers.ParserConfigurationException;
52  import javax.xml.parsers.SAXParser;
53  import javax.xml.parsers.SAXParserFactory;
54  import javax.xml.transform.OutputKeys;
55  import javax.xml.transform.Transformer;
56  import javax.xml.transform.TransformerFactory;
57  import javax.xml.transform.dom.DOMSource;
58  import javax.xml.transform.stream.StreamResult;
59  import org.apache.commons.logging.Log;
60  import org.apache.commons.logging.LogFactory;
61  import org.apache.geronimo.gbean.GBeanInfo;
62  import org.apache.geronimo.gbean.GBeanInfoBuilder;
63  import org.apache.geronimo.kernel.config.ConfigurationData;
64  import org.apache.geronimo.kernel.config.ConfigurationManager;
65  import org.apache.geronimo.kernel.config.ConfigurationStore;
66  import org.apache.geronimo.kernel.config.InvalidConfigException;
67  import org.apache.geronimo.kernel.config.NoSuchConfigException;
68  import org.apache.geronimo.kernel.repository.Artifact;
69  import org.apache.geronimo.kernel.repository.ArtifactResolver;
70  import org.apache.geronimo.kernel.repository.DefaultArtifactResolver;
71  import org.apache.geronimo.kernel.repository.Dependency;
72  import org.apache.geronimo.kernel.repository.FileWriteMonitor;
73  import org.apache.geronimo.kernel.repository.ImportType;
74  import org.apache.geronimo.kernel.repository.MissingDependencyException;
75  import org.apache.geronimo.kernel.repository.Repository;
76  import org.apache.geronimo.kernel.repository.Version;
77  import org.apache.geronimo.kernel.repository.WritableListableRepository;
78  import org.apache.geronimo.kernel.InvalidGBeanException;
79  import org.apache.geronimo.kernel.util.XmlUtil;
80  import org.apache.geronimo.system.configuration.ConfigurationStoreUtil;
81  import org.apache.geronimo.system.configuration.GBeanOverride;
82  import org.apache.geronimo.system.configuration.PluginAttributeStore;
83  import org.apache.geronimo.system.serverinfo.ServerInfo;
84  import org.apache.geronimo.system.threads.ThreadPool;
85  import org.apache.geronimo.util.encoders.Base64;
86  import org.w3c.dom.Document;
87  import org.w3c.dom.Element;
88  import org.w3c.dom.Node;
89  import org.w3c.dom.NodeList;
90  import org.xml.sax.Attributes;
91  import org.xml.sax.ErrorHandler;
92  import org.xml.sax.SAXException;
93  import org.xml.sax.SAXParseException;
94  import org.xml.sax.helpers.DefaultHandler;
95  
96  /**
97   * A GBean that knows how to download configurations from a Maven repository.
98   *
99   * @version $Rev: 470597 $ $Date: 2006-11-02 15:30:55 -0800 (Thu, 02 Nov 2006) $
100  */
101 public class PluginInstallerGBean implements PluginInstaller {
102     private final static Log log = LogFactory.getLog(PluginInstallerGBean.class);
103     private static int counter;
104     private ConfigurationManager configManager;
105     private WritableListableRepository writeableRepo;
106     private ConfigurationStore configStore;
107     private ArtifactResolver resolver;
108     private ServerInfo serverInfo;
109     private Map asyncKeys;
110     private ThreadPool threadPool;
111     private PluginAttributeStore attributeStore;
112 
113     public PluginInstallerGBean(ConfigurationManager configManager, WritableListableRepository repository, ConfigurationStore configStore, ServerInfo serverInfo, ThreadPool threadPool, PluginAttributeStore store) {
114         this.configManager = configManager;
115         this.writeableRepo = repository;
116         this.configStore = configStore;
117         this.serverInfo = serverInfo;
118         this.threadPool = threadPool;
119         resolver = new DefaultArtifactResolver(null, writeableRepo);
120         asyncKeys = Collections.synchronizedMap(new HashMap());
121         attributeStore = store;
122     }
123 
124     /**
125      * Lists the plugins installed in the local Geronimo server, by name and
126      * ID.
127      *
128      * @return A Map with key type String (plugin name) and value type Artifact
129      *         (config ID of the plugin).
130      */
131     public Map getInstalledPlugins() {
132         SortedSet artifacts = writeableRepo.list();
133 
134         Map plugins = new HashMap();
135         for (Iterator i = artifacts.iterator(); i.hasNext();) {
136             Artifact configId = (Artifact) i.next();
137             File dir = writeableRepo.getLocation(configId);
138             if(dir.isDirectory()) {
139                 File meta = new File(dir, "META-INF");
140                 if(!meta.isDirectory() || !meta.canRead()) {
141                     continue;
142                 }
143                 File xml = new File(meta, "geronimo-plugin.xml");
144                 if(!xml.isFile() || !xml.canRead() || xml.length() == 0) {
145                     continue;
146                 }
147                 readNameAndID(xml, plugins);
148             } else {
149                 if(!dir.isFile() || !dir.canRead()) {
150                     throw new IllegalStateException("Cannot read artifact dir "+dir.getAbsolutePath());
151                 }
152                 try {
153                     JarFile jar = new JarFile(dir);
154                     try {
155                         ZipEntry entry = jar.getEntry("META-INF/geronimo-plugin.xml");
156                         if(entry == null) {
157                             continue;
158                         }
159                         InputStream in = jar.getInputStream(entry);
160                         readNameAndID(in, plugins);
161                         in.close();
162                     } finally {
163                         jar.close();
164                     }
165                 } catch (IOException e) {
166                     log.error("Unable to read JAR file "+dir.getAbsolutePath(), e);
167                 }
168             }
169         }
170         return plugins;
171     }
172 
173     /**
174      * Gets a CofigurationMetadata for a configuration installed in the local
175      * server.  Should load a saved one if available, or else create a new
176      * default one to the best of its abilities.
177      *
178      * @param moduleId Identifies the configuration.  This must match a
179      *                 configuration currently installed in the local server.
180      *                 The configId must be fully resolved (isResolved() == true)
181      */
182     public PluginMetadata getPluginMetadata(Artifact moduleId) {
183         if(configManager != null) {
184             if(!configManager.isConfiguration(moduleId)) {
185                 return null;
186             }
187         } else {
188             if(!configStore.containsConfiguration(moduleId)) {
189                 return null;
190             }
191         }
192         File dir = writeableRepo.getLocation(moduleId);
193         Document doc;
194         ConfigurationData configData;
195         String source = dir.getAbsolutePath();
196         try {
197             if(dir.isDirectory()) {
198                 File meta = new File(dir, "META-INF");
199                 if(!meta.isDirectory() || !meta.canRead()) {
200                     return null;
201                 }
202                 File xml = new File(meta, "geronimo-plugin.xml");
203                 configData = configStore.loadConfiguration(moduleId);
204                 if(!xml.isFile() || !xml.canRead() || xml.length() == 0) {
205                     return createDefaultMetadata(configData);
206                 }
207                 source = xml.getAbsolutePath();
208                 DocumentBuilder builder = createDocumentBuilder();
209                 doc = builder.parse(xml);
210             } else {
211                 if(!dir.isFile() || !dir.canRead()) {
212                     throw new IllegalStateException("Cannot read configuration "+dir.getAbsolutePath());
213                 }
214                 configData = configStore.loadConfiguration(moduleId);
215                 JarFile jar = new JarFile(dir);
216                 try {
217                     ZipEntry entry = jar.getEntry("META-INF/geronimo-plugin.xml");
218                     if(entry == null) {
219                         return createDefaultMetadata(configData);
220                     }
221                     source = dir.getAbsolutePath()+"#META-INF/geronimo-plugin.xml";
222                     InputStream in = jar.getInputStream(entry);
223                     DocumentBuilder builder = createDocumentBuilder();
224                     doc = builder.parse(in);
225                     in.close();
226                 } finally {
227                     jar.close();
228                 }
229             }
230             PluginMetadata result = loadPluginMetadata(doc, source);
231             overrideDependencies(configData, result);
232             return result;
233         } catch (InvalidConfigException e) {
234             e.printStackTrace();
235             log.warn("Unable to generate metadata for "+moduleId, e);
236         } catch (Exception e) {
237             e.printStackTrace();
238             log.warn("Invalid XML at "+source, e);
239         }
240         return null;
241     }
242 
243     /**
244      * Saves a ConfigurationMetadata for a particular plugin, if the server is
245      * able to record it.  This can be used if you later re-export the plugin,
246      * or just want to review the information for a particular installed
247      * plugin.
248      *
249      * @param metadata The data to save.  The contained configId (which must
250      *                 be fully resolved) identifies the configuration to save
251      *                 this for.
252      */
253     public void updatePluginMetadata(PluginMetadata metadata) {
254         File dir = writeableRepo.getLocation(metadata.getModuleId());
255         if(dir == null) {
256             throw new IllegalArgumentException(metadata.getModuleId()+" is not installed!");
257         }
258         if(!dir.isDirectory()) { // must be a packed (JAR-formatted) plugin
259             try {
260                 File temp = new File(dir.getParentFile(), dir.getName()+".temp");
261                 JarFile input = new JarFile(dir);
262                 Manifest manifest = input.getManifest();
263                 JarOutputStream out = manifest == null ? new JarOutputStream(new BufferedOutputStream(new FileOutputStream(temp)))
264                         : new JarOutputStream(new BufferedOutputStream(new FileOutputStream(temp)), manifest);
265                 Enumeration en = input.entries();
266                 byte[] buf = new byte[4096];
267                 int count;
268                 while (en.hasMoreElements()) {
269                     JarEntry entry = (JarEntry) en.nextElement();
270                     if(entry.getName().equals("META-INF/geronimo-plugin.xml")) {
271                         entry = new JarEntry(entry.getName());
272                         out.putNextEntry(entry);
273                         Document doc = writePluginMetadata(metadata);
274                         TransformerFactory xfactory = XmlUtil.newTransformerFactory();
275                         Transformer xform = xfactory.newTransformer();
276                         xform.setOutputProperty(OutputKeys.INDENT, "yes");
277                         xform.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
278                         xform.transform(new DOMSource(doc), new StreamResult(out));
279                     } else if(entry.getName().equals("META-INF/MANIFEST.MF")) {
280                         // do nothing, already passed in a manifest
281                     } else {
282                         out.putNextEntry(entry);
283                         InputStream in = input.getInputStream(entry);
284                         while((count = in.read(buf)) > -1) {
285                             out.write(buf, 0, count);
286                         }
287                         in.close();
288                         out.closeEntry();
289                     }
290                 }
291                 out.flush();
292                 out.close();
293                 input.close();
294                 if(!dir.delete()) {
295                     throw new IOException("Unable to delete old plugin at "+dir.getAbsolutePath());
296                 }
297                 if(!temp.renameTo(dir)) {
298                     throw new IOException("Unable to move new plugin "+temp.getAbsolutePath()+" to "+dir.getAbsolutePath());
299                 }
300             } catch (Exception e) {
301                 log.error("Unable to update plugin metadata", e);
302                 throw new RuntimeException("Unable to update plugin metadata", e);
303             } // TODO this really should have a finally block to ensure streams are closed
304         } else {
305             File meta = new File(dir, "META-INF");
306             if(!meta.isDirectory() || !meta.canRead()) {
307                 throw new IllegalArgumentException(metadata.getModuleId()+" is not a plugin!");
308             }
309             File xml = new File(meta, "geronimo-plugin.xml");
310             FileOutputStream fos = null;
311             try {
312                 if(!xml.isFile()) {
313                     if(!xml.createNewFile()) {
314                         throw new RuntimeException("Cannot create plugin metadata file for "+metadata.getModuleId());
315                     }
316                 }
317                 Document doc = writePluginMetadata(metadata);
318                 TransformerFactory xfactory = XmlUtil.newTransformerFactory();
319                 Transformer xform = xfactory.newTransformer();
320                 xform.setOutputProperty(OutputKeys.INDENT, "yes");
321                 xform.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
322                 fos = new FileOutputStream(xml);
323                 // use a FileOutputStream instead of a File on the StreamResult 
324                 // constructor as problems were encountered with the file not being closed.
325                 StreamResult sr = new StreamResult(fos); 
326                 xform.transform(new DOMSource(doc), sr);
327             } catch (Exception e) {
328                 log.error("Unable to save plugin metadata for "+metadata.getModuleId(), e);
329             } finally {
330                 if (fos != null) {
331                     try {
332                         fos.close();
333                     } catch (IOException ignored) {
334                         // ignored
335                     }
336                 }
337             }
338         }
339     }
340 
341     /**
342      * Lists the plugins available for download in a particular Geronimo repository.
343      *
344      * @param mavenRepository The base URL to the maven repository.  This must
345      *                        contain the file geronimo-plugins.xml
346      * @param username Optional username, if the maven repo uses HTTP Basic authentication.
347      *                 Set this to null if no authentication is required.
348      * @param password Optional password, if the maven repo uses HTTP Basic authentication.
349      *                 Set this to null if no authentication is required.
350      */
351     public PluginList listPlugins(URL mavenRepository, String username, String password) throws IOException, FailedLoginException {
352         String repository = mavenRepository.toString();
353         if(!repository.endsWith("/")) {
354             repository = repository+"/";
355         }
356         //todo: Try downloading a .gz first
357         URL url = new URL(repository+"geronimo-plugins.xml");
358         try {
359             //todo: use a progress monitor
360             InputStream in = openStream(null, new URL[]{url}, username, password, null).getStream();
361             return loadPluginList(mavenRepository, in);
362         } catch (MissingDependencyException e) {
363             log.error("Cannot find plugin index at site "+url);
364             return null;
365         } catch (Exception e) {
366             log.error("Unable to load repository configuration data", e);
367             return null;
368         }
369     }
370 
371     /**
372      * Installs a configuration from a remote repository into the local Geronimo server,
373      * including all its dependencies.  The caller will get the results when the
374      * operation completes.  Note that this method does not throw exceptions on failure,
375      * but instead sets the failure property of the DownloadResults.
376      *
377      * @param username         Optional username, if the maven repo uses HTTP Basic authentication.
378      *                         Set this to null if no authentication is required.
379      * @param password         Optional password, if the maven repo uses HTTP Basic authentication.
380      *                         Set this to null if no authentication is required.
381      * @param pluginsToInstall The list of configurations to install
382      */
383     public DownloadResults install(PluginList pluginsToInstall, String username, String password) {
384         DownloadResults results = new DownloadResults();
385         install(pluginsToInstall, username, password, results);
386         return results;
387     }
388 
389     /**
390      * Installs a configuration from a remote repository into the local Geronimo server,
391      * including all its dependencies.  The method blocks until the operation completes,
392      * but the caller will be notified of progress frequently along the way (using the
393      * supplied DownloadPoller).  Therefore the caller is meant to create the poller and
394      * then call this method in a background thread.  Note that this method does not
395      * throw exceptions on failure, but instead sets the failure property of the
396      * DownloadPoller.
397      *
398      * @param pluginsToInstall The list of configurations to install
399      * @param username         Optional username, if the maven repo uses HTTP Basic authentication.
400      *                         Set this to null if no authentication is required.
401      * @param password         Optional password, if the maven repo uses HTTP Basic authentication.
402      *                         Set this to null if no authentication is required.
403      * @param poller           Will be notified with status updates as the download proceeds
404      */
405     public void install(PluginList pluginsToInstall, String username, String password, DownloadPoller poller) {
406         try {
407             Map metaMap = new HashMap();
408             // Step 1: validate everything
409             for (int i = 0; i < pluginsToInstall.getPlugins().length; i++) {
410                 PluginMetadata metadata = pluginsToInstall.getPlugins()[i];
411                 validatePlugin(metadata);
412                 if(metadata.getModuleId() != null) {
413                     metaMap.put(metadata.getModuleId(), metadata);
414                 }
415             }
416 
417             // Step 2: everything is valid, do the installation
418             for (int i = 0; i < pluginsToInstall.getPlugins().length; i++) {
419                 // 1. Identify the configuration
420                 PluginMetadata metadata = pluginsToInstall.getPlugins()[i];
421                 // 2. Unload obsoleted configurations
422                 List obsoletes = new ArrayList();
423                 for (int j = 0; j < metadata.getObsoletes().length; j++) {
424                     String name = metadata.getObsoletes()[j];
425                     Artifact obsolete = Artifact.create(name);
426                     Artifact[] list = configManager.getArtifactResolver().queryArtifacts(obsolete);
427                     for (int k = 0; k < list.length; k++) {
428                         Artifact artifact = list[k];
429                         if(configManager.isLoaded(artifact)) {
430                             if(configManager.isRunning(artifact)) {
431                                 configManager.stopConfiguration(artifact);
432                             }
433                             configManager.unloadConfiguration(artifact);
434                             obsoletes.add(artifact);
435                         }
436                     }
437                 }
438                 // 3. Download the artifact if necessary, and its dependencies
439                 Set working = new HashSet();
440                 if(metadata.getModuleId() != null) {
441                     URL[] repos = pluginsToInstall.getRepositories();
442                     if(metadata.getRepositories().length > 0) {
443                         repos = metadata.getRepositories();
444                     }
445                     downloadArtifact(metadata.getModuleId(), metaMap, repos,
446                             username, password, new ResultsFileWriteMonitor(poller), working, false);
447                 } else {
448                     String[] deps = metadata.getDependencies();
449                     for (int j = 0; j < deps.length; j++) {
450                         String dep = deps[j];
451                         Artifact entry = Artifact.create(dep);
452                         URL[] repos = pluginsToInstall.getRepositories();
453                         if(metadata.getRepositories().length > 0) {
454                             repos = metadata.getRepositories();
455                         }
456                         downloadArtifact(entry, metaMap, repos,
457                                 username, password, new ResultsFileWriteMonitor(poller), working, false);
458                     }
459                 }
460                 // 4. Uninstall obsolete configurations
461                 for (int j = 0; j < obsoletes.size(); j++) {
462                     Artifact artifact = (Artifact) obsoletes.get(j);
463                     configManager.uninstallConfiguration(artifact);
464                 }
465                 // 5. Installation of this configuration finished successfully
466             }
467 
468             // Step 3: Start anything that's marked accordingly
469             for (int i = 0; i < pluginsToInstall.getPlugins().length; i++) {
470                 PluginMetadata metadata = pluginsToInstall.getPlugins()[i];
471                 for (int j = 0; j < metadata.getForceStart().length; j++) {
472                     String id = metadata.getForceStart()[j];
473                     Artifact artifact = Artifact.create(id);
474                     if(configManager.isConfiguration(artifact)) {
475                         poller.setCurrentFilePercent(-1);
476                         poller.setCurrentMessage("Starting "+artifact);
477                         configManager.loadConfiguration(artifact);
478                         configManager.startConfiguration(artifact);
479                     }
480                 }
481             }
482         } catch (Exception e) {
483             poller.setFailure(e);
484         } finally {
485             poller.setFinished();
486         }
487     }
488 
489     /**
490      * Installs a configuration from a remote repository into the local Geronimo server,
491      * including all its dependencies.  The method returns immediately, providing a key
492      * that can be used to poll the status of the download operation.  Note that the
493      * installation does not throw exceptions on failure, but instead sets the failure
494      * property of the DownloadResults that the caller can poll for.
495      *
496      * @param pluginsToInstall The list of configurations to install
497      * @param username         Optional username, if the maven repo uses HTTP Basic authentication.
498      *                         Set this to null if no authentication is required.
499      * @param password         Optional password, if the maven repo uses HTTP Basic authentication.
500      *                         Set this to null if no authentication is required.
501      *
502      * @return A key that can be passed to checkOnInstall
503      */
504     public Object startInstall(final PluginList pluginsToInstall, final String username, final String password) {
505         Object key = getNextKey();
506         final DownloadResults results = new DownloadResults();
507         Runnable work = new Runnable() {
508             public void run() {
509                 install(pluginsToInstall, username, password, results);
510             }
511         };
512         asyncKeys.put(key, results);
513         try {
514             threadPool.execute("Configuration Installer", work);
515         } catch (InterruptedException e) {
516             throw new RuntimeException("Unable to start work", e);
517         }
518         return key;
519     }
520 
521     /**
522      * Installs a configuration downloaded from a remote repository into the local Geronimo
523      * server, including all its dependencies.  The method returns immediately, providing a
524      * key that can be used to poll the status of the download operation.  Note that the
525      * installation does not throw exceptions on failure, but instead sets the failure
526      * property of the DownloadResults that the caller can poll for.
527      *
528      * @param carFile   A CAR file downloaded from a remote repository.  This is a packaged
529      *                  configuration with included configuration information, but it may
530      *                  still have external dependencies that need to be downloaded
531      *                  separately.  The metadata in the CAR file includes a repository URL
532      *                  for these downloads, and the username and password arguments are
533      *                  used in conjunction with that.
534      * @param username  Optional username, if the maven repo uses HTTP Basic authentication.
535      *                  Set this to null if no authentication is required.
536      * @param password  Optional password, if the maven repo uses HTTP Basic authentication.
537      *                  Set this to null if no authentication is required.
538      *
539      * @return A key that can be passed to checkOnInstall
540      */
541     public Object startInstall(final File carFile, final String username, final String password) {
542         Object key = getNextKey();
543         final DownloadResults results = new DownloadResults();
544         Runnable work = new Runnable() {
545             public void run() {
546                 install(carFile, username, password, results);
547             }
548         };
549         asyncKeys.put(key, results);
550         try {
551             threadPool.execute("Configuration Installer", work);
552         } catch (InterruptedException e) {
553             throw new RuntimeException("Unable to start work", e);
554         }
555         return key;
556     }
557 
558     /**
559      * Gets the current progress of a download operation.  Note that once the
560      * DownloadResults is returned for this operation shows isFinished = true,
561      * the operation will be forgotten, so the caller should be careful not to
562      * call this again after the download has finished.
563      *
564      * @param key Identifies the operation to check on
565      */
566     public DownloadResults checkOnInstall(Object key) {
567         DownloadResults results = (DownloadResults) asyncKeys.get(key);
568         results = results.duplicate();
569         if(results.isFinished()) {
570             asyncKeys.remove(key);
571         }
572         return results;
573     }
574 
575     /**
576      * Installs from a pre-downloaded CAR file
577      */
578     public void install(File carFile, String username, String password, DownloadPoller poller) {
579         try {
580             // 1. Extract the configuration metadata
581             PluginMetadata data = loadCARFile(carFile, true);
582             if(data == null) {
583                 throw new IllegalArgumentException("Invalid Configuration Archive "+carFile.getAbsolutePath()+" see server log for details");
584             }
585 
586             // 2. Validate that we can install this
587             validatePlugin(data);
588 
589             // 3. Install the CAR into the repository (it shouldn't be re-downloaded)
590             if(data.getModuleId() != null) {
591                 ResultsFileWriteMonitor monitor = new ResultsFileWriteMonitor(poller);
592                 writeableRepo.copyToRepository(carFile, data.getModuleId(), monitor);
593                 installConfigXMLData(data.getModuleId(), data);
594                 if(data.getFilesToCopy() != null) {
595                     extractPluginFiles(data.getModuleId(), data, monitor);
596                 }
597             }
598 
599             // 4. Use the standard logic to remove obsoletes, install dependencies, etc.
600             //    This will validate all over again (oh, well)
601             install(new PluginList(data.getRepositories(), new PluginMetadata[]{data}),
602                     username, password, poller);
603         } catch (Exception e) {
604             poller.setFailure(e);
605         } finally {
606             poller.setFinished();
607         }
608     }
609 
610     /**
611      * Ensures that a plugin is installable.
612      */
613     private void validatePlugin(PluginMetadata metadata) throws MissingDependencyException {
614         // 1. Check that it's not already running
615         if(metadata.getModuleId() != null) { // that is, it's a real configuration not a plugin list
616             if(configManager.isRunning(metadata.getModuleId())) {
617                 boolean upgrade = false;
618                 for (int i = 0; i < metadata.getObsoletes().length; i++) {
619                     String obsolete = metadata.getObsoletes()[i];
620                     Artifact test = Artifact.create(obsolete);
621                     if(test.matches(metadata.getModuleId())) {
622                         upgrade = true;
623                         break;
624                     }
625                 }
626                 if(!upgrade) {
627                     throw new IllegalArgumentException("Configuration "+metadata.getModuleId()+" is already running!");
628                 }
629             }
630         }
631         // 2. Check that we meet the prerequisites
632         PluginMetadata.Prerequisite[] prereqs = metadata.getPrerequisites();
633         for (int i = 0; i < prereqs.length; i++) {
634             PluginMetadata.Prerequisite prereq = prereqs[i];
635             if(resolver.queryArtifacts(prereq.getModuleId()).length == 0) {
636                 throw new MissingDependencyException("Required configuration '"+prereq.getModuleId()+"' is not installed.");
637             }
638         }
639         // 3. Check that we meet the Geronimo, JVM versions
640         if(metadata.getGeronimoVersions().length > 0 && !checkGeronimoVersions(metadata.getGeronimoVersions())) {
641             throw new MissingDependencyException("Cannot install plugin "+metadata.getModuleId()+" on Geronimo "+serverInfo.getVersion());
642         }
643         if(metadata.getJvmVersions().length > 0 && !checkJVMVersions(metadata.getJvmVersions())) {
644             throw new MissingDependencyException("Cannot install plugin "+metadata.getModuleId()+" on JVM "+System.getProperty("java.version"));
645         }
646     }
647 
648     /**
649      * Download (if necessary) and install something, which may be a Configuration or may
650      * be just a JAR.  For each artifact processed, all its dependencies will be
651      * processed as well.
652      *
653      * @param configID  Identifies the artifact to install
654      * @param repos     The URLs to contact the repositories (in order of preference)
655      * @param username  The username used for repositories secured with HTTP Basic authentication
656      * @param password  The password used for repositories secured with HTTP Basic authentication
657      * @param monitor   The ongoing results of the download operations, with some monitoring logic
658      *
659      * @throws IOException                 When there's a problem reading or writing data
660      * @throws FailedLoginException        When a repository requires authentication and either no username
661      *                                     and password are supplied or the username and password supplied
662      *                                     are not accepted
663      * @throws MissingDependencyException  When a dependency cannot be located in any of the listed repositories
664      */
665     private void downloadArtifact(Artifact configID, Map metadata, URL[] repos, String username, String password, ResultsFileWriteMonitor monitor, Set soFar, boolean dependency) throws IOException, FailedLoginException, MissingDependencyException {
666         if(soFar.contains(configID)) {
667             return; // Avoid enless work due to circular dependencies
668         } else {
669             soFar.add(configID);
670         }
671         // Download and install the main artifact
672         boolean pluginWasInstalled = false;
673         Artifact[] matches = configManager.getArtifactResolver().queryArtifacts(configID);
674         if(matches.length == 0) { // not present, needs to be downloaded
675             monitor.getResults().setCurrentMessage("Downloading " + configID);
676             monitor.getResults().setCurrentFilePercent(-1);
677             OpenResult result = openStream(configID, repos, username, password, monitor);
678             try {
679                 File tempFile = downloadFile(result, monitor);
680                 PluginMetadata pluginData = ((PluginMetadata) metadata.get(configID));
681                 // Only bother with the hash if we got it from a source other than the download file itself
682                 PluginMetadata.Hash hash = pluginData == null ? null : pluginData.getHash();
683                 if(hash != null) {
684                     String actual = ConfigurationStoreUtil.getActualChecksum(tempFile, hash.getType());
685                     if(!actual.equals(hash.getValue())) {
686                         throw new IOException("File download incorrect (expected "+hash.getType()+" hash "+hash.getValue()+" but got "+actual+")");
687                     }
688                 }
689                 // See if the download file has plugin metadata
690                 if(pluginData == null) {
691                     try {
692                         pluginData = loadCARFile(tempFile, false);
693                     } catch (Exception e) {
694                         throw new IOException("Unable to read plugin metadata: "+e.getMessage());
695                     }
696                 }
697                 if(pluginData != null) { // it's a plugin, not a plain JAR
698                     validatePlugin(pluginData);
699                 }
700                 monitor.getResults().setCurrentMessage("Copying " + result.getConfigID() + " to the repository");
701                 writeableRepo.copyToRepository(tempFile, result.getConfigID(), monitor); //todo: download SNAPSHOTS if previously available?
702                 if(!tempFile.delete()) {
703                     log.warn("Unable to delete temporary download file "+tempFile.getAbsolutePath());
704                     tempFile.deleteOnExit();
705                 }
706                 installConfigXMLData(result.getConfigID(), pluginData);
707                 if(dependency) {
708                     monitor.getResults().addDependencyInstalled(configID);
709                     configID = result.getConfigID();
710                 } else {
711                     configID = result.getConfigID();
712                     monitor.getResults().addInstalledConfigID(configID);
713                 }
714                 pluginWasInstalled = true;
715             } finally {
716                 result.getStream().close();
717             }
718         } else {
719             if(dependency) {
720                 monitor.getResults().addDependencyPresent(configID);
721             } else {
722                 monitor.getResults().addInstalledConfigID(configID);
723             }
724         }
725         // Download and install the dependencies
726         try {
727             ConfigurationData data = null;
728             if(!configID.isResolved()) {
729                 // See if something's running
730                 for (int i = matches.length-1; i >= 0; i--) {
731                     Artifact match = matches[i];
732                     if(configStore.containsConfiguration(match) && configManager.isRunning(match)) {
733                         return; // its dependencies must be OK
734                     }
735                 }
736                 // Go with something that's installed
737                 configID = matches[matches.length-1];
738             }
739             if(configStore.containsConfiguration(configID)) {
740                 if(configManager.isRunning(configID)) {
741                     return; // its dependencies must be OK
742                 }
743                 data = configStore.loadConfiguration(configID);
744             }
745             Dependency[] dependencies = data == null ? getDependencies(writeableRepo, configID) : getDependencies(data);
746             // Download the dependencies
747             for (int i = 0; i < dependencies.length; i++) {
748                 Dependency dep = dependencies[i];
749                 Artifact artifact = dep.getArtifact();
750                 downloadArtifact(artifact, metadata, repos, username, password, monitor, soFar, true);
751             }
752         } catch (NoSuchConfigException e) {
753             throw new IllegalStateException("Installed configuration into repository but ConfigStore does not see it: "+e.getMessage());
754         } catch (InvalidConfigException e) {
755             throw new IllegalStateException("Installed configuration into repository but ConfigStore cannot load it: "+e.getMessage());
756         }
757         // Copy any files out of the artifact
758         PluginMetadata currentPlugin = configManager.isConfiguration(configID) ? getPluginMetadata(configID) : null;
759         if(pluginWasInstalled && currentPlugin != null && currentPlugin.getFilesToCopy() != null) {
760             extractPluginFiles(configID, currentPlugin, monitor);
761         }
762     }
763 
764     private void extractPluginFiles(Artifact configID, PluginMetadata currentPlugin, ResultsFileWriteMonitor monitor) throws IOException {
765         for (int i = 0; i < currentPlugin.getFilesToCopy().length; i++) {
766             PluginMetadata.CopyFile data = currentPlugin.getFilesToCopy()[i];
767             monitor.getResults().setCurrentFilePercent(-1);
768             monitor.getResults().setCurrentFile(data.getSourceFile());
769             monitor.getResults().setCurrentMessage("Copying "+data.getSourceFile()+" from plugin to Geronimo installation");
770             Set set;
771             try {
772                 set = configStore.resolve(configID, null, data.getSourceFile());
773             } catch (NoSuchConfigException e) {
774                 throw new IllegalStateException("Unable to identify module "+configID+" to copy files from");
775             }
776             if(set.size() == 0) {
777                 log.error("Installed configuration into repository but cannot locate file to copy "+data.getSourceFile());
778                 continue;
779             }
780             File targetDir = data.isRelativeToVar() ? serverInfo.resolveServer("var/"+data.getDestDir()) : serverInfo.resolve(data.getDestDir());
781             if(!targetDir.isDirectory()) {
782                 log.error("Plugin install cannot write file "+data.getSourceFile()+" to "+data.getDestDir()+" because "+targetDir.getAbsolutePath()+" is not a directory");
783                 continue;
784             }
785             if(!targetDir.canWrite()) {
786                 log.error("Plugin install cannot write file "+data.getSourceFile()+" to "+data.getDestDir()+" because "+targetDir.getAbsolutePath()+" is not writable");
787                 continue;
788             }
789             for (Iterator it = set.iterator(); it.hasNext();) {
790                 URL url = (URL) it.next();
791                 String path = url.getPath();
792                 if(path.lastIndexOf('/') > -1) {
793                     path = path.substring(path.lastIndexOf('/'));
794                 }
795                 File target = new File(targetDir, path);
796                 if(!target.exists()) {
797                     if(!target.createNewFile()) {
798                         log.error("Plugin install cannot create new file "+target.getAbsolutePath());
799                         continue;
800                     }
801                 }
802                 if(!target.canWrite()) {
803                     log.error("Plugin install cannot write to file "+target.getAbsolutePath());
804                     continue;
805                 }
806                 copyFile(url.openStream(), new FileOutputStream(target));
807             }
808         }
809     }
810 
811     private void copyFile(InputStream in, FileOutputStream out) throws IOException {
812         byte[] buf = new byte[4096];
813         int count;
814         while((count = in.read(buf)) > -1) {
815             out.write(buf, 0, count);
816         }
817         in.close();
818         out.flush();
819         out.close();
820     }
821 
822     /**
823      * Downloads to a temporary file so we can validate the download before
824      * installing into the repository.
825      */
826     private File downloadFile(OpenResult result, ResultsFileWriteMonitor monitor) throws IOException {
827         InputStream in = result.getStream();
828         if(in == null) {
829             throw new IllegalStateException();
830         }
831         FileOutputStream out = null;
832         try {        
833             monitor.writeStarted(result.getConfigID().toString(), result.fileSize);
834             File file = File.createTempFile("geronimo-plugin-download-", ".tmp");
835             out = new FileOutputStream(file);
836             byte[] buf = new byte[4096];
837             int count, total = 0;
838             while((count = in.read(buf)) > -1) {
839                 out.write(buf, 0, count);
840                 monitor.writeProgress(total += count);
841             }
842             monitor.writeComplete(total);
843             in.close();
844             in = null;
845             out.close();
846             out = null;
847             return file;            
848         } finally {
849             if (in != null) {
850                 try {
851                     in.close();
852                 } catch (IOException ignored) { }
853             }
854             if (out != null) {
855                 try {
856                     out.close();
857                 } catch (IOException ignored) { }
858             }
859         }
860     }
861 
862     /**
863      * Used to get dependencies for a JAR
864      */
865     private static Dependency[] getDependencies(Repository repo, Artifact artifact) {
866         Set set = repo.getDependencies(artifact);
867         Dependency[] results = new Dependency[set.size()];
868         int index=0;
869         for (Iterator it = set.iterator(); it.hasNext(); ++index) {
870             Artifact dep = (Artifact) it.next();
871             results[index] = new Dependency(dep, ImportType.CLASSES);
872         }
873         return results;
874     }
875 
876     /**
877      * Used to get dependencies for a Configuration
878      */
879     private static Dependency[] getDependencies(ConfigurationData data) {
880         List dependencies = new ArrayList(data.getEnvironment().getDependencies());
881         Collection children = data.getChildConfigurations().values();
882         for (Iterator it = children.iterator(); it.hasNext();) {
883             ConfigurationData child = (ConfigurationData) it.next();
884             dependencies.addAll(child.getEnvironment().getDependencies());
885         }
886         return (Dependency[]) dependencies.toArray(new Dependency[dependencies.size()]);
887     }
888 
889     /**
890      * Constructs a URL to a particular artifact in a particular repository
891      */
892     private static URL getURL(Artifact configId, URL repository) throws MalformedURLException {
893 	URL context;
894 	if(repository.toString().endsWith("/")) {
895 	    context = repository;
896 	} else {
897 	    context = new URL(repository.toString()+"/");
898 	}
899 
900 	String qualifiedVersion = configId.getVersion().toString();
901 	if (configId.getVersion() instanceof SnapshotVersion) {
902             SnapshotVersion ssVersion = (SnapshotVersion)configId.getVersion();
903             String timestamp = ssVersion.getTimestamp();
904             int buildNumber = ssVersion.getBuildNumber();
905             if (timestamp!=null && buildNumber!=0) {
906                 qualifiedVersion = qualifiedVersion.replaceAll("SNAPSHOT", timestamp + "-" + buildNumber);
907             }
908 	}
909         return new URL(context, configId.getGroupId().replace('.','/') + "/"
910                      + configId.getArtifactId() + "/" + configId.getVersion()
911                      + "/" +configId.getArtifactId() + "-"
912                      + qualifiedVersion + "." +configId.getType());
913     }
914 
915     /**
916      * Attemps to open a stream to an artifact in one of the listed repositories.
917      * The username and password provided are only used if one of the repositories
918      * returns an HTTP authentication failure on the first try.
919      *
920      * @param artifact  The artifact we're looking for, or null to just connect to the base repo URL
921      * @param repos     The base URLs to the repositories to search for the artifact
922      * @param username  A username if one of the repositories might require authentication
923      * @param password  A password if one of the repositories might require authentication
924      * @param monitor   Callback for progress on the connection operation
925      *
926      * @throws IOException Occurs when the IO with the repository failed
927      * @throws FailedLoginException Occurs when a repository requires authentication and either
928      *                              no username and password were provided or they weren't
929      *                              accepted
930      * @throws MissingDependencyException Occurs when none of the repositories has the artifact
931      *                                    in question
932      */
933     private static OpenResult openStream(Artifact artifact, URL[] repos, String username, String password, ResultsFileWriteMonitor monitor) throws IOException, FailedLoginException, MissingDependencyException {
934         if(artifact != null) {
935             if (!artifact.isResolved() || artifact.getVersion().toString().indexOf("SNAPSHOT") >= 0) {
936                 artifact = findArtifact(artifact, repos, username, password, monitor);
937             }
938         }
939         if(monitor != null) {
940             monitor.getResults().setCurrentFilePercent(-1);
941             monitor.getResults().setCurrentMessage("Downloading "+artifact+"...");
942             monitor.setTotalBytes(-1); // In case the server doesn't say
943         }
944         InputStream in;
945         LinkedList list = new LinkedList();
946         list.addAll(Arrays.asList(repos));
947         while (true) {
948             if(list.isEmpty()) {
949                 throw new MissingDependencyException("Unable to download dependency "+artifact);
950             }
951             if(monitor != null) {
952                 monitor.setTotalBytes(-1); // Just to be sure
953             }
954             URL repository = (URL) list.removeFirst();
955             URL url = artifact == null ? repository : getURL(artifact, repository);
956             log.debug("Attempting to download "+artifact+" from "+url);
957             in = connect(url, username, password, monitor);
958             if(in != null) {
959                 return new OpenResult(artifact, in, monitor == null ? -1 : monitor.getTotalBytes());
960             }
961         }
962     }
963 
964     /**
965      * Does the meat of connecting to a URL
966      */
967     private static InputStream connect(URL url, String username, String password, ResultsFileWriteMonitor monitor) throws IOException, FailedLoginException {
968         return connect(url, username, password, monitor, null);
969     }
970 
971     /**
972      * Does the meat of connecting to a URL.  Can be used to just test the existance of
973      * something at the specified URL by passing the method 'HEAD'.
974      */
975     private static InputStream connect(URL url, String username, String password, ResultsFileWriteMonitor monitor, String method) throws IOException, FailedLoginException {
976         URLConnection con = url.openConnection();
977         if(con instanceof HttpURLConnection) {
978             HttpURLConnection http = (HttpURLConnection) url.openConnection();
979             if(method != null) {
980                 http.setRequestMethod(method);
981             }
982             http.connect();
983             if(http.getResponseCode() == 401) { // need to authenticate
984                 if(username == null || username.equals("")) {
985                     throw new FailedLoginException("Server returned 401 "+http.getResponseMessage());
986                 }
987                 http = (HttpURLConnection) url.openConnection();
988                 http.setRequestProperty("Authorization", "Basic " + new String(Base64.encode((username + ":" + password).getBytes())));
989                 if(method != null) {
990                     http.setRequestMethod(method);
991                 }
992                 http.connect();
993                 if(http.getResponseCode() == 401) {
994                     throw new FailedLoginException("Server returned 401 "+http.getResponseMessage());
995                 } else if(http.getResponseCode() == 404) {
996                     return null; // Not found at this repository
997                 }
998                 if(monitor != null && http.getContentLength() > 0) {
999                     monitor.setTotalBytes(http.getContentLength());
1000                 }
1001                 return http.getInputStream();
1002             } else if(http.getResponseCode() == 404) {
1003                 return null; // Not found at this repository
1004             } else {
1005                 if(monitor != null && http.getContentLength() > 0) {
1006                     monitor.setTotalBytes(http.getContentLength());
1007                 }
1008                 return http.getInputStream();
1009             }
1010         } else {
1011             if(username != null && !username.equals("")) {
1012                 con.setRequestProperty("Authorization", "Basic " + new String(Base64.encode((username + ":" + password).getBytes())));
1013                 try {
1014                     con.connect();
1015                     if(monitor != null && con.getContentLength() > 0) {
1016                         monitor.setTotalBytes(con.getContentLength());
1017                     }
1018                     return con.getInputStream();
1019                 } catch (FileNotFoundException e) {
1020                     return null;
1021                 }
1022             } else {
1023                 try {
1024                     con.connect();
1025                     if(monitor != null && con.getContentLength() > 0) {
1026                         monitor.setTotalBytes(con.getContentLength());
1027                     }
1028                     return con.getInputStream();
1029                 } catch (FileNotFoundException e) {
1030                     return null;
1031                 }
1032             }
1033         }
1034     }
1035 
1036     /**
1037      * Searches for an artifact in the listed repositories, where the artifact
1038      * may have wildcards in the ID.
1039      */
1040     private static Artifact findArtifact(Artifact query, URL[] repos, String username, String password, ResultsFileWriteMonitor monitor) throws MissingDependencyException {
1041         if(query.getGroupId() == null || query.getArtifactId() == null || query.getType() == null) {
1042             throw new MissingDependencyException("No support yet for dependencies missing more than a version: "+query);
1043         }
1044         List list = new ArrayList();
1045         for (int i = 0; i < repos.length; i++) {
1046             list.add(repos[i]);
1047         }
1048         Artifact result = null;
1049         for (int i = 0; i < list.size(); i++) {
1050             URL url = (URL) list.get(i);
1051             try {
1052                 result = findArtifact(query, url, username, password, monitor);
1053             } catch (Exception e) {
1054                 log.warn("Unable to read from "+url, e);
1055             }
1056             if(result != null) {
1057                 return result;
1058             }
1059         }
1060         throw new MissingDependencyException("No repository has a valid artifact for "+query);
1061     }
1062 
1063     /**
1064      * Checks for an artifact in a specific repository, where the artifact may
1065      * have wildcards in the ID.
1066      */
1067     private static Artifact findArtifact(Artifact query, URL url, String username, String password, ResultsFileWriteMonitor monitor) throws IOException, FailedLoginException, ParserConfigurationException, SAXException {
1068         monitor.getResults().setCurrentMessage("Searching for "+query+" at "+url);
1069         String base = query.getGroupId().replace('.', '/') + "/" + query.getArtifactId();
1070         String path = base +"/maven-metadata.xml";
1071         URL metaURL = new URL(url.toString().endsWith("/") ? url : new URL(url.toString()+"/"), path);
1072         InputStream in = connect(metaURL, username, password, monitor);
1073         if(in == null) {
1074             return null;
1075         }
1076         // Don't use the validating parser that we normally do
1077         DocumentBuilder builder = XmlUtil.newDocumentBuilderFactory().newDocumentBuilder();
1078         Document doc = builder.parse(in);
1079         Element root = doc.getDocumentElement();
1080         NodeList list = root.getElementsByTagName("versions");
1081         if(list.getLength() == 0) {
1082             return null;
1083         }
1084         list = ((Element)list.item(0)).getElementsByTagName("version");
1085         Version[] available = new Version[list.getLength()];
1086         for (int i = 0; i < available.length; i++) {
1087             available[i] = new Version(getText(list.item(i)));
1088         }
1089         Arrays.sort(available);
1090         for(int i=available.length-1; i>=0; i--) {
1091             Version version = available[i];
1092             URL metadataURL = new URL(url.toString()+base+"/"+version+"/maven-metadata.xml");
1093             InputStream metadataStream = connect(metadataURL, username, password, monitor);
1094             
1095             // check for a snapshot qualifier
1096             if (metadataStream != null) {
1097                 DocumentBuilder metadatabuilder = XmlUtil.newDocumentBuilderFactory().newDocumentBuilder();
1098                 Document metadatadoc = metadatabuilder.parse(metadataStream);
1099                 NodeList snapshots = metadatadoc.getDocumentElement().getElementsByTagName("snapshot");
1100                 if (snapshots.getLength() >= 1) {
1101                     Element snapshot = (Element)snapshots.item(0);
1102                     String[] timestamp = getChildrenText(snapshot, "timestamp");
1103                     String[] buildNumber = getChildrenText(snapshot, "buildNumber");
1104                     if (timestamp.length>=1 && buildNumber.length>=1) {
1105                         try {
1106                             SnapshotVersion snapshotVersion = new SnapshotVersion(version);
1107                             snapshotVersion.setBuildNumber(Integer.parseInt(buildNumber[0]));
1108                             snapshotVersion.setTimestamp(timestamp[0]);
1109                             version = snapshotVersion;
1110                         } catch (NumberFormatException nfe) {
1111                             log.warn("Could not create snapshot version for " + query);
1112                         }
1113                     }
1114                 }
1115                 metadataStream.close();
1116             }
1117             
1118             // look for the artifact in the maven repo
1119             Artifact verifiedArtifact = new Artifact(query.getGroupId(), query.getArtifactId(), version, query.getType()); 
1120             URL test = getURL(verifiedArtifact, url);
1121             InputStream testStream = connect(test, username, password, monitor, "HEAD");
1122             if(testStream == null) {
1123                 log.warn("Maven repository "+url+" listed artifact "+query+" version "+version+" but I couldn't find it at "+test);
1124                 continue;
1125             }
1126             testStream.close();
1127             return verifiedArtifact; 
1128         }
1129         return null;
1130     }
1131 
1132     /**
1133      * Puts the name and ID of a plugin into the argument map of plugins,
1134      * by reading the values out of the provided plugin descriptor file.
1135      *
1136      * @param xml     The geronimo-plugin.xml for this plugin
1137      * @param plugins The result map to populate
1138      */
1139     private void readNameAndID(File xml, Map plugins) {
1140         try {
1141             SAXParserFactory factory = XmlUtil.newSAXParserFactory();
1142             SAXParser parser = factory.newSAXParser();
1143             PluginNameIDHandler handler = new PluginNameIDHandler();
1144             parser.parse(xml, handler);
1145             if(handler.isComplete()) {
1146                 plugins.put(handler.getName(), Artifact.create(handler.getID()));
1147             }
1148         } catch (Exception e) {
1149             log.warn("Invalid XML at "+xml.getAbsolutePath(), e);
1150         }
1151     }
1152 
1153     /**
1154      * Puts the name and ID of a plugin into the argument map of plugins,
1155      * by reading the values out of the provided plugin descriptor stream.
1156      *
1157      * @param xml     The geronimo-plugin.xml for this plugin
1158      * @param plugins The result map to populate
1159      */
1160     private void readNameAndID(InputStream xml, Map plugins) {
1161         try {
1162             SAXParserFactory factory = XmlUtil.newSAXParserFactory();
1163             SAXParser parser = factory.newSAXParser();
1164             PluginNameIDHandler handler = new PluginNameIDHandler();
1165             parser.parse(xml, handler);
1166             if(handler.isComplete()) {
1167                 plugins.put(handler.getName(), Artifact.create(handler.getID()));
1168             }
1169         } catch (Exception e) {
1170             log.warn("Invalid XML", e);
1171         }
1172     }
1173 
1174     /**
1175      * Replaces all the dependency elements in the argument configuration data
1176      * with the dependencies from the actual data for that module.
1177      */
1178     private void overrideDependencies(ConfigurationData data, PluginMetadata metadata) {
1179         //todo: this ends up doing a little more work than necessary
1180         PluginMetadata temp = createDefaultMetadata(data);
1181         metadata.setDependencies(temp.getDependencies());
1182     }
1183 
1184     /**
1185      * Generates a default plugin metadata based on the data for this module
1186      * in the server.
1187      */
1188     private PluginMetadata createDefaultMetadata(ConfigurationData data) {
1189         PluginMetadata meta = new PluginMetadata(data.getId().toString(), // name
1190                 data.getId(), // module ID
1191                 "Unknown", // category
1192                 "Please provide a description",
1193                 null, // URL
1194                 null, // author
1195                 null, // hash
1196                 true, // installed
1197                 false);
1198         meta.setGeronimoVersions(new String[]{serverInfo.getVersion()});
1199         meta.setJvmVersions(new String[0]);
1200         meta.setLicenses(new PluginMetadata.License[0]);
1201         meta.setObsoletes(new String[]{new Artifact(data.getId().getGroupId(), data.getId().getArtifactId(), (Version)null, data.getId().getType()).toString()});
1202         meta.setFilesToCopy(new PluginMetadata.CopyFile[0]);
1203         List deps = new ArrayList();
1204         PluginMetadata.Prerequisite prereq = null;
1205         prereq = processDependencyList(data.getEnvironment().getDependencies(), prereq, deps);
1206         Map children = data.getChildConfigurations();
1207         for (Iterator it = children.values().iterator(); it.hasNext();) {
1208             ConfigurationData child = (ConfigurationData) it.next();
1209             prereq = processDependencyList(child.getEnvironment().getDependencies(), prereq, deps);
1210         }
1211         meta.setDependencies((String[]) deps.toArray(new String[deps.size()]));
1212         meta.setPrerequisites(prereq == null ? new PluginMetadata.Prerequisite[0] : new PluginMetadata.Prerequisite[]{prereq});
1213         return meta;
1214     }
1215 
1216     /**
1217      * Read the plugin metadata out of a plugin CAR file on disk.
1218      */
1219     private PluginMetadata loadCARFile(File file, boolean definitelyCAR) throws IOException, ParserConfigurationException, SAXException {
1220         if(!file.canRead()) {
1221             log.error("Cannot read from downloaded CAR file "+file.getAbsolutePath());
1222             return null;
1223         }
1224         JarFile jar = new JarFile(file);
1225         Document doc;
1226         try {
1227             JarEntry entry = jar.getJarEntry("META-INF/geronimo-plugin.xml");
1228             if(entry == null) {
1229                 if(definitelyCAR) {
1230                     log.error("Downloaded CAR file does not contain META-INF/geronimo-plugin.xml file");
1231                 }
1232                 jar.close();
1233                 return null;
1234             }
1235             InputStream in = jar.getInputStream(entry);
1236             DocumentBuilder builder = createDocumentBuilder();
1237             doc = builder.parse(in);
1238             in.close();
1239         } finally {
1240             jar.close();
1241         }
1242         return loadPluginMetadata(doc, file.getAbsolutePath());
1243     }
1244 
1245     /**
1246      * Read a set of plugin metadata from a DOM document.
1247      */
1248     private PluginMetadata loadPluginMetadata(Document doc, String file) throws SAXException, MalformedURLException {
1249         Element root = doc.getDocumentElement();
1250         if(!root.getNodeName().equals("geronimo-plugin")) {
1251             log.error("Configuration archive "+file+" does not have a geronimo-plugin in META-INF/geronimo-plugin.xml");
1252             return null;
1253         }
1254         return processPlugin(root);
1255     }
1256 
1257     /**
1258      * Loads the list of all available plugins from the specified stream
1259      * (representing geronimo-plugins.xml at the specified repository).
1260      */
1261     private PluginList loadPluginList(URL repo, InputStream in) throws ParserConfigurationException, IOException, SAXException {
1262         DocumentBuilder builder = createDocumentBuilder();
1263         Document doc = builder.parse(in);
1264         in.close();
1265         Element root = doc.getDocumentElement(); // geronimo-plugin-list
1266         NodeList configs = root.getElementsByTagName("plugin");
1267         List results = new ArrayList();
1268         for (int i = 0; i < configs.getLength(); i++) {
1269             Element config = (Element) configs.item(i);
1270             PluginMetadata data = processPlugin(config);
1271             results.add(data);
1272         }
1273         String[] repos = getChildrenText(root, "default-repository");
1274         URL[] repoURLs = new URL[repos.length];
1275         for(int i = 0; i < repos.length; i++) {
1276             if(repos[i].endsWith("/")) {
1277                 repoURLs[i] = new URL(repos[i]);
1278             } else {
1279                 repoURLs[i] = new URL(repos[i]+"/");
1280             }
1281         }
1282 
1283         PluginMetadata[] data = (PluginMetadata[]) results.toArray(new PluginMetadata[results.size()]);
1284         return new PluginList(repoURLs, data);
1285     }
1286 
1287     /**
1288      * Common logic for setting up a document builder to deal with plugin files.
1289      * @return
1290      * @throws ParserConfigurationException
1291      */
1292     private static DocumentBuilder createDocumentBuilder() throws ParserConfigurationException {
1293         DocumentBuilderFactory factory = XmlUtil.newDocumentBuilderFactory();
1294         factory.setValidating(true);
1295         factory.setNamespaceAware(true);
1296         factory.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaLanguage",
1297                              "http://www.w3.org/2001/XMLSchema");
1298         factory.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaSource",
1299                              new InputStream[]{
1300                                      PluginInstallerGBean.class.getResourceAsStream("/META-INF/schema/attributes-1.1.xsd"),
1301                                      PluginInstallerGBean.class.getResourceAsStream("/META-INF/schema/plugins-1.1.xsd"),
1302                              }
1303         );
1304         DocumentBuilder builder = factory.newDocumentBuilder();
1305         builder.setErrorHandler(new ErrorHandler() {
1306             public void error(SAXParseException exception) throws SAXException {
1307                 throw new SAXException("Unable to read plugin file", exception);
1308             }
1309 
1310             public void fatalError(SAXParseException exception) throws SAXException {
1311                 throw new SAXException("Unable to read plugin file", exception);
1312             }
1313 
1314             public void warning(SAXParseException exception) {
1315                 log.warn("Warning reading XML document", exception);
1316             }
1317         });
1318         return builder;
1319     }
1320 
1321     /**
1322      * Given a DOM element representing a plugin, load it into a PluginMetadata
1323      * object.
1324      */
1325     private PluginMetadata processPlugin(Element plugin) throws SAXException, MalformedURLException {
1326         String moduleId = getChildText(plugin, "module-id");
1327         NodeList licenseNodes = plugin.getElementsByTagName("license");
1328         PluginMetadata.License[] licenses = new PluginMetadata.License[licenseNodes.getLength()];
1329         for(int j=0; j<licenseNodes.getLength(); j++) {
1330             Element node = (Element) licenseNodes.item(j);
1331             String licenseName = getText(node);
1332             String openSource = node.getAttribute("osi-approved");
1333             if(licenseName == null || licenseName.equals("") || openSource == null || openSource.equals("")) {
1334                 throw new SAXException("Invalid config file: license name and osi-approved flag required");
1335             }
1336             licenses[j] = new PluginMetadata.License(licenseName, Boolean.valueOf(openSource).booleanValue());
1337         }
1338         PluginMetadata.Hash hash = null;
1339         NodeList hashList = plugin.getElementsByTagName("hash");
1340         if(hashList.getLength() > 0) {
1341             Element elem = (Element) hashList.item(0);
1342             hash = new PluginMetadata.Hash(elem.getAttribute("type"), getText(elem));
1343         }
1344         NodeList fileList = plugin.getElementsByTagName("copy-file");
1345         PluginMetadata.CopyFile[] files = new PluginMetadata.CopyFile[fileList.getLength()];
1346         for (int i = 0; i < files.length; i++) {
1347             Element node = (Element) fileList.item(i);
1348             String relative = node.getAttribute("relative-to");
1349             String destDir = node.getAttribute("dest-dir");
1350             String fileName = getText(node);
1351             files[i] = new PluginMetadata.CopyFile(relative.equals("server"), fileName, destDir);
1352         }
1353         NodeList gbeans = plugin.getElementsByTagName("gbean");
1354         GBeanOverride[] overrides = new GBeanOverride[gbeans.getLength()];
1355         for (int i = 0; i < overrides.length; i++) {
1356             Element node = (Element) gbeans.item(i);
1357             try {
1358                 overrides[i] = new GBeanOverride(node);
1359             } catch (InvalidGBeanException e) {
1360                 log.error("Unable to process config.xml entry "+node.getAttribute("name")+" ("+node+")", e);
1361             }
1362         }
1363         boolean eligible = true;
1364         NodeList preNodes = plugin.getElementsByTagName("prerequisite");
1365         PluginMetadata.Prerequisite[] prereqs = new PluginMetadata.Prerequisite[preNodes.getLength()];
1366         for(int j=0; j<preNodes.getLength(); j++) {
1367             Element node = (Element) preNodes.item(j);
1368             String originalConfigId = getChildText(node, "id");
1369             if(originalConfigId == null) {
1370                 throw new SAXException("Prerequisite requires <id>");
1371             }
1372             Artifact artifact = Artifact.create(originalConfigId.replaceAll("\\*", ""));
1373             boolean present = resolver.queryArtifacts(artifact).length > 0;
1374             prereqs[j] = new PluginMetadata.Prerequisite(artifact, present,
1375                     getChildText(node, "resource-type"), getChildText(node, "description"));
1376             if(!present) {
1377                 log.debug(moduleId+" is not eligible due to missing "+prereqs[j].getModuleId());
1378                 eligible = false;
1379             }
1380         }
1381         String[] gerVersions = getChildrenText(plugin, "geronimo-version");
1382         if(gerVersions.length > 0) {
1383             boolean match = checkGeronimoVersions(gerVersions);
1384             if(!match) eligible = false;
1385         }
1386         String[] jvmVersions = getChildrenText(plugin, "jvm-version");
1387         if(jvmVersions.length > 0) {
1388             boolean match = checkJVMVersions(jvmVersions);
1389             if(!match) eligible = false;
1390         }
1391         String[] repoNames = getChildrenText(plugin, "source-repository");
1392         URL[] repos = new URL[repoNames.length];
1393         for (int i = 0; i < repos.length; i++) {
1394             repos[i] = new URL(repoNames[i]);
1395         }
1396         Artifact artifact = null;
1397         boolean installed = false;
1398         if (moduleId != null) {
1399             artifact = Artifact.create(moduleId);
1400             // Tests, etc. don't need to have a ConfigurationManager
1401             installed = configManager != null && configManager.isInstalled(artifact);
1402         }
1403         log.trace("Checking "+moduleId+": installed="+installed+", eligible="+eligible);
1404         PluginMetadata data = new PluginMetadata(getChildText(plugin, "name"),
1405                 artifact,
1406                 getChildText(plugin, "category"),
1407                 getChildText(plugin, "description"),
1408                 getChildText(plugin, "url"),
1409                 getChildText(plugin, "author"),
1410                 hash,
1411                 installed, eligible);
1412         data.setGeronimoVersions(gerVersions);
1413         data.setJvmVersions(jvmVersions);
1414         data.setLicenses(licenses);
1415         data.setPrerequisites(prereqs);
1416         data.setRepositories(repos);
1417         data.setFilesToCopy(files);
1418         data.setConfigXmls(overrides);
1419         NodeList list = plugin.getElementsByTagName("dependency");
1420         List start = new ArrayList();
1421         String deps[] = new String[list.getLength()];
1422         for(int i=0; i<list.getLength(); i++) {
1423             Element node = (Element) list.item(i);
1424             deps[i] = getText(node);
1425             if(node.hasAttribute("start") && node.getAttribute("start").equalsIgnoreCase("true")) {
1426                 start.add(deps[i]);
1427             }
1428         }
1429         data.setDependencies(deps);
1430         data.setForceStart((String[]) start.toArray(new String[start.size()]));
1431         data.setObsoletes(getChildrenText(plugin, "obsoletes"));
1432         return data;
1433     }
1434 
1435     /**
1436      * Check whether the specified JVM versions match the current runtime
1437      * environment.
1438      *
1439      * @return true if the specified versions match the current
1440      *              execution environment as defined by plugins-1.1.xsd
1441      */
1442     private boolean checkJVMVersions(String[] jvmVersions) {
1443         if(jvmVersions.length == 0) return true;
1444         String version = System.getProperty("java.version");
1445         boolean match = false;
1446         for (int j = 0; j < jvmVersions.length; j++) {
1447             String jvmVersion = jvmVersions[j];
1448             if(jvmVersion == null || jvmVersion.equals("")) {
1449                 throw new IllegalStateException("jvm-version should not be empty!");
1450             }
1451             if(version.startsWith(jvmVersion)) {
1452                 match = true;
1453                 break;
1454             }
1455         }
1456         return match;
1457     }
1458 
1459     /**
1460      * Check whether the specified Geronimo versions match the current runtime
1461      * environment.
1462      *
1463      * @return true if the specified versions match the current
1464      *              execution environment as defined by plugins-1.1.xsd
1465      */
1466     private boolean checkGeronimoVersions(String[] gerVersions) {
1467         if(gerVersions.length == 0) return true;
1468         String version = serverInfo.getVersion();
1469         boolean match = false;
1470         for (int j = 0; j < gerVersions.length; j++) {
1471             String gerVersion = gerVersions[j];
1472             if(gerVersion == null || gerVersion.equals("")) {
1473                 throw new IllegalStateException("geronimo-version should not be empty!");
1474             }
1475             if(gerVersion.equals(version)) {
1476                 match = true;
1477                 break;
1478             }
1479         }
1480         return match;
1481     }
1482 
1483     /**
1484      * Gets the text out of a child of the specified DOM element.
1485      *
1486      * @param root      The parent DOM element
1487      * @param property  The name of the child element that holds the text
1488      */
1489     private static String getChildText(Element root, String property) {
1490         NodeList children = root.getChildNodes();
1491         for(int i=0; i<children.getLength(); i++) {
1492             Node check = children.item(i);
1493             if(check.getNodeType() == Node.ELEMENT_NODE && check.getNodeName().equals(property)) {
1494                 return getText(check);
1495             }
1496         }
1497         return null;
1498     }
1499 
1500     /**
1501      * Gets all the text contents of the specified DOM node.
1502      */
1503     private static String getText(Node target) {
1504         NodeList nodes = target.getChildNodes();
1505         StringBuffer buf = null;
1506         for(int j=0; j<nodes.getLength(); j++) {
1507             Node node = nodes.item(j);
1508             if(node.getNodeType() == Node.TEXT_NODE) {
1509                 if(buf == null) {
1510                     buf = new StringBuffer();
1511                 }
1512                 buf.append(node.getNodeValue());
1513             }
1514         }
1515         return buf == null ? null : buf.toString();
1516     }
1517 
1518     /**
1519      * Gets the text out of all the child nodes of a certain type.  The result
1520      * array has one element for each child of the specified DOM element that
1521      * has the specified name.
1522      *
1523      * @param root      The parent DOM element
1524      * @param property  The name of the child elements that hold the text
1525      */
1526     private static String[] getChildrenText(Element root, String property) {
1527         NodeList children = root.getChildNodes();
1528         List results = new ArrayList();
1529         for(int i=0; i<children.getLength(); i++) {
1530             Node check = children.item(i);
1531             if(check.getNodeType() == Node.ELEMENT_NODE && check.getNodeName().equals(property)) {
1532                 NodeList nodes = check.getChildNodes();
1533                 StringBuffer buf = null;
1534                 for(int j=0; j<nodes.getLength(); j++) {
1535                     Node node = nodes.item(j);
1536                     if(node.getNodeType() == Node.TEXT_NODE) {
1537                         if(buf == null) {
1538                             buf = new StringBuffer();
1539                         }
1540                         buf.append(node.getNodeValue());
1541                     }
1542                 }
1543                 results.add(buf == null ? null : buf.toString());
1544             }
1545         }
1546         return (String[]) results.toArray(new String[results.size()]);
1547     }
1548 
1549     /**
1550      * Generates dependencies and an optional prerequisite based on a list of
1551      * dependencies for a Gernonimo module.
1552      *
1553      * @param real   A list with elements of type Dependency
1554      * @param prereq The incoming prerequisite (if any), which may be replaced
1555      * @param deps   A list with elements of type String (holding a module ID / Artifact name)
1556      *
1557      * @return The resulting prerequisite, if any.
1558      */
1559     private PluginMetadata.Prerequisite processDependencyList(List real, PluginMetadata.Prerequisite prereq, List deps) {
1560         for (int i = 0; i < real.size(); i++) {
1561             Dependency dep = (Dependency) real.get(i);
1562             if(dep.getArtifact().getGroupId().equals("geronimo")) {
1563                 if(dep.getArtifact().getArtifactId().indexOf("jetty") > -1) {
1564                     if(prereq == null) {
1565                         prereq = new PluginMetadata.Prerequisite(dep.getArtifact(), true, "Web Container", "This plugin works with the Geronimo/Jetty distribution.  It is not intended to run in the Geronimo/Tomcat distribution.  There is a separate version of this plugin that works with Tomcat.");
1566                     }
1567                     continue;
1568                 } else if(dep.getArtifact().getArtifactId().indexOf("tomcat") > -1) {
1569                     if(prereq == null) {
1570                         prereq = new PluginMetadata.Prerequisite(dep.getArtifact(), true, "Web Container", "This plugin works with the Geronimo/Tomcat distribution.  It is not intended to run in the Geronimo/Jetty distribution.  There is a separate version of this plugin that works with Jetty.");
1571                     }
1572                     continue;
1573                 }
1574             }
1575             if(!deps.contains(dep.getArtifact().toString())) {
1576                 deps.add(dep.getArtifact().toString());
1577             }
1578         }
1579         return prereq;
1580     }
1581 
1582     /**
1583      * Writes plugin metadata to a DOM tree.
1584      */
1585     private static Document writePluginMetadata(PluginMetadata data) throws ParserConfigurationException {
1586         DocumentBuilder builder = createDocumentBuilder();
1587         Document doc = builder.newDocument();
1588         Element config = doc.createElementNS("http://geronimo.apache.org/xml/ns/plugins-1.1", "geronimo-plugin");
1589         config.setAttribute("xmlns", "http://geronimo.apache.org/xml/ns/plugins-1.1");
1590         doc.appendChild(config);
1591 
1592         addTextChild(doc, config, "name", data.getName());
1593         addTextChild(doc, config, "module-id", data.getModuleId().toString());
1594         addTextChild(doc, config, "category", data.getCategory());
1595         addTextChild(doc, config, "description", data.getDescription());
1596         if(data.getPluginURL() != null) {
1597             addTextChild(doc, config, "url", data.getPluginURL());
1598         }
1599         if(data.getAuthor() != null) {
1600             addTextChild(doc, config, "author", data.getAuthor());
1601         }
1602         for (int i = 0; i < data.getLicenses().length; i++) {
1603             PluginMetadata.License license = data.getLicenses()[i];
1604             Element lic = doc.createElement("license");
1605             lic.appendChild(doc.createTextNode(license.getName()));
1606             lic.setAttribute("osi-approved", Boolean.toString(license.isOsiApproved()));
1607             config.appendChild(lic);
1608         }
1609         if(data.getHash() != null) {
1610             Element hash = doc.createElement("hash");
1611             hash.setAttribute("type", data.getHash().getType());
1612             hash.appendChild(doc.createTextNode(data.getHash().getValue()));
1613             config.appendChild(hash);
1614         }
1615         for (int i = 0; i < data.getGeronimoVersions().length; i++) {
1616             addTextChild(doc, config, "geronimo-version", data.getGeronimoVersions()[i]);
1617         }
1618         for (int i = 0; i < data.getJvmVersions().length; i++) {
1619             addTextChild(doc, config, "jvm-version", data.getJvmVersions()[i]);
1620         }
1621         for (int i = 0; i < data.getPrerequisites().length; i++) {
1622             PluginMetadata.Prerequisite prereq = data.getPrerequisites()[i];
1623             Element pre = doc.createElement("prerequisite");
1624             addTextChild(doc, pre, "id", prereq.getModuleId().toString());
1625             if(prereq.getResourceType() != null) {
1626                 addTextChild(doc, pre, "resource-type", prereq.getResourceType());
1627             }
1628             if(prereq.getDescription() != null) {
1629                 addTextChild(doc, pre, "description", prereq.getDescription());
1630             }
1631             config.appendChild(pre);
1632         }
1633         for (int i = 0; i < data.getDependencies().length; i++) {
1634             addTextChild(doc, config, "dependency", data.getDependencies()[i]);
1635         }
1636         for (int i = 0; i < data.getObsoletes().length; i++) {
1637             addTextChild(doc, config, "obsoletes", data.getObsoletes()[i]);
1638         }
1639         for (int i = 0; i < data.getRepositories().length; i++) {
1640             URL url = data.getRepositories()[i];
1641             addTextChild(doc, config, "source-repository", url.toString());
1642         }
1643         for (int i = 0; i < data.getFilesToCopy().length; i++) {
1644             PluginMetadata.CopyFile file = data.getFilesToCopy()[i];
1645             Element copy = doc.createElement("copy-file");
1646             copy.setAttribute("relative-to", file.isRelativeToVar() ? "server" : "geronimo");
1647             copy.setAttribute("dest-dir", file.getDestDir());
1648             copy.appendChild(doc.createTextNode(file.getSourceFile()));
1649             config.appendChild(copy);
1650         }
1651         if(data.getConfigXmls().length > 0) {
1652             Element content = doc.createElement("config-xml-content");
1653             for (int i = 0; i < data.getConfigXmls().length; i++) {
1654                 GBeanOverride override = data.getConfigXmls()[i];
1655                 Element gbean = override.writeXml(doc, content);
1656                 gbean.setAttribute("xmlns", "http://geronimo.apache.org/xml/ns/attributes-1.1");
1657             }
1658             config.appendChild(content);
1659         }
1660         return doc;
1661     }
1662 
1663     /**
1664      * Adds a child of the specified Element that just has the specified text content
1665      * @param doc     The document
1666      * @param parent  The parent element
1667      * @param name    The name of the child element to add
1668      * @param text    The contents of the child element to add
1669      */
1670     private static void addTextChild(Document doc, Element parent, String name, String text) {
1671         Element child = doc.createElement(name);
1672         child.appendChild(doc.createTextNode(text));
1673         parent.appendChild(child);
1674     }
1675 
1676     /**
1677      * If a plugin includes config.xml content, copy it into the attribute
1678      * store.
1679      */
1680     private void installConfigXMLData(Artifact configID, PluginMetadata pluginData) {
1681         if(configManager.isConfiguration(configID) && attributeStore != null
1682                 && pluginData != null && pluginData.getConfigXmls().length > 0) {
1683             attributeStore.setModuleGBeans(configID, pluginData.getConfigXmls());
1684         }
1685     }
1686 
1687     /**
1688      * Gets a token unique to this run of the server, used to track asynchronous
1689      * downloads.
1690      */
1691     private static Object getNextKey() {
1692         int value;
1693         synchronized(PluginInstallerGBean.class) {
1694             value = ++counter;
1695         }
1696         return new Integer(value);
1697     }
1698 
1699     /**
1700      * Helper clas to extract a name and module ID from a plugin metadata file.
1701      */
1702     private static class PluginNameIDHandler extends DefaultHandler {
1703         private String id = "";
1704         private String name = "";
1705         private String element = null;
1706 
1707         public void characters(char ch[], int start, int length) throws SAXException {
1708             if(element != null) {
1709                 if(element.equals("module-id")) {
1710                     id += new String(ch, start, length);
1711                 } else if(element.equals("name")) {
1712                     name += new String(ch, start, length);
1713                 }
1714             }
1715         }
1716 
1717         public void endElement(String uri, String localName, String qName) throws SAXException {
1718             element = null;
1719         }
1720 
1721         public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
1722             if(qName.equals("module-id") || qName.equals("name")) {
1723                 element = qName;
1724             }
1725         }
1726 
1727         public void endDocument() throws SAXException {
1728             id = id.trim();
1729             name = name.trim();
1730         }
1731 
1732         public String getID() {
1733             return id;
1734         }
1735 
1736         public String getName() {
1737             return name;
1738         }
1739 
1740         public boolean isComplete() {
1741             return !id.equals("") && !name.equals("");
1742         }
1743     }
1744 
1745     /**
1746      * Helper class to bridge a FileWriteMonitor to a DownloadPoller.
1747      */
1748     private static class ResultsFileWriteMonitor implements FileWriteMonitor {
1749         private final DownloadPoller results;
1750         private int totalBytes;
1751         private String file;
1752 
1753         public ResultsFileWriteMonitor(DownloadPoller results) {
1754             this.results = results;
1755         }
1756 
1757         public void setTotalBytes(int totalBytes) {
1758             this.totalBytes = totalBytes;
1759         }
1760 
1761         public int getTotalBytes() {
1762             return totalBytes;
1763         }
1764 
1765         public void writeStarted(String fileDescription, int fileSize) {
1766             totalBytes = fileSize;
1767             file = fileDescription;
1768             results.setCurrentFile(fileDescription);
1769             results.setCurrentFilePercent(totalBytes > 0 ? 0 : -1);
1770         }
1771 
1772         public void writeProgress(int bytes) {
1773             if(totalBytes > 0) {
1774                 double percent = (double)bytes/(double)totalBytes;
1775                 results.setCurrentFilePercent((int)(percent*100));
1776             } else {
1777                 results.setCurrentMessage((bytes/1024)+" kB of "+file);
1778             }
1779         }
1780 
1781         public void writeComplete(int bytes) {
1782             results.setCurrentFilePercent(100);
1783             results.setCurrentMessage("Finished installing "+file+" ("+(bytes/1024)+" kB)");
1784             results.addDownloadBytes(bytes);
1785         }
1786 
1787         public DownloadPoller getResults() {
1788             return results;
1789         }
1790     }
1791 
1792     /**
1793      * Interesting data resulting from opening a connection to a remote file.
1794      */
1795     private static class OpenResult {
1796         private final InputStream stream;
1797         private final Artifact configID;
1798         private final int fileSize;
1799 
1800         public OpenResult(Artifact configID, InputStream stream, int fileSize) {
1801             this.configID = configID;
1802             this.stream = stream;
1803             this.fileSize = fileSize;
1804         }
1805 
1806         public Artifact getConfigID() {
1807             return configID;
1808         }
1809 
1810         public InputStream getStream() {
1811             return stream;
1812         }
1813 
1814         public int getFileSize() {
1815             return fileSize;
1816         }
1817     }
1818 
1819     public static final GBeanInfo GBEAN_INFO;
1820 
1821     static {
1822         GBeanInfoBuilder infoFactory = GBeanInfoBuilder.createStatic(PluginInstallerGBean.class);
1823         infoFactory.addReference("ConfigManager", ConfigurationManager.class, "ConfigurationManager");
1824         infoFactory.addReference("Repository", WritableListableRepository.class, "Repository");
1825         infoFactory.addReference("ConfigStore", ConfigurationStore.class, "ConfigurationStore");
1826         infoFactory.addReference("ServerInfo", ServerInfo.class, "GBean");
1827         infoFactory.addReference("ThreadPool", ThreadPool.class, "GBean");
1828         infoFactory.addReference("PluginAttributeStore", PluginAttributeStore.class, "AttributeStore");
1829         infoFactory.addInterface(PluginInstaller.class);
1830 
1831         infoFactory.setConstructor(new String[]{"ConfigManager", "Repository", "ConfigStore",
1832                                                 "ServerInfo", "ThreadPool", "PluginAttributeStore"});
1833 
1834         GBEAN_INFO = infoFactory.getBeanInfo();
1835     }
1836 
1837     public static GBeanInfo getGBeanInfo() {
1838         return GBEAN_INFO;
1839     }
1840 }