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