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