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