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