001    /**
002     *
003     * Copyright 2003-2004 The Apache Software Foundation
004     *
005     *  Licensed under the Apache License, Version 2.0 (the "License");
006     *  you may not use this file except in compliance with the License.
007     *  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    
018    package org.apache.geronimo.deployment;
019    
020    import java.io.File;
021    import java.io.IOException;
022    import java.net.URI;
023    import java.util.ArrayList;
024    import java.util.Collection;
025    import java.util.Collections;
026    import java.util.Enumeration;
027    import java.util.Hashtable;
028    import java.util.Iterator;
029    import java.util.List;
030    import java.util.Properties;
031    import java.util.Set;
032    import java.util.jar.Attributes;
033    import java.util.jar.JarFile;
034    import java.util.jar.Manifest;
035    
036    import javax.management.ObjectName;
037    
038    import org.apache.commons.logging.Log;
039    import org.apache.commons.logging.LogFactory;
040    import org.apache.geronimo.common.DeploymentException;
041    import org.apache.geronimo.deployment.util.DeploymentUtil;
042    import org.apache.geronimo.gbean.AbstractName;
043    import org.apache.geronimo.gbean.AbstractNameQuery;
044    import org.apache.geronimo.gbean.GBeanInfo;
045    import org.apache.geronimo.gbean.GBeanInfoBuilder;
046    import org.apache.geronimo.kernel.GBeanNotFoundException;
047    import org.apache.geronimo.kernel.Kernel;
048    import org.apache.geronimo.kernel.config.Configuration;
049    import org.apache.geronimo.kernel.config.ConfigurationData;
050    import org.apache.geronimo.kernel.config.ConfigurationManager;
051    import org.apache.geronimo.kernel.config.ConfigurationStore;
052    import org.apache.geronimo.kernel.config.ConfigurationUtil;
053    import org.apache.geronimo.kernel.config.InvalidConfigException;
054    import org.apache.geronimo.kernel.config.DeploymentWatcher;
055    import org.apache.geronimo.kernel.repository.Artifact;
056    import org.apache.geronimo.kernel.repository.ArtifactResolver;
057    import org.apache.geronimo.system.configuration.ExecutableConfigurationUtil;
058    import org.apache.geronimo.system.main.CommandLineManifest;
059    
060    /**
061     * GBean that knows how to deploy modules (by consulting available module builders)
062     *
063     * @version $Rev: 427123 $ $Date: 2006-07-31 07:16:46 -0700 (Mon, 31 Jul 2006) $
064     */
065    public class Deployer {
066        private static final Log log = LogFactory.getLog(Deployer.class);
067        private final int REAPER_INTERVAL = 60 * 1000;
068        private final Properties pendingDeletionIndex = new Properties();
069        private DeployerReaper reaper;
070        private final Collection builders;
071        private final Collection stores;
072        private final Collection watchers;
073        private final ArtifactResolver artifactResolver;
074        private final Kernel kernel;
075    
076        public Deployer(Collection builders, Collection stores, Collection watchers, Kernel kernel) {
077            this(builders, stores, watchers, getArtifactResolver(kernel), kernel);
078        }
079    
080        private static ArtifactResolver getArtifactResolver(Kernel kernel) {
081            ConfigurationManager configurationManager = ConfigurationUtil.getConfigurationManager(kernel);
082            return configurationManager.getArtifactResolver();
083        }
084    
085        public Deployer(Collection builders, Collection stores, Collection watchers, ArtifactResolver artifactResolver, Kernel kernel) {
086            this.builders = builders;
087            this.stores = stores;
088            this.watchers = watchers;
089            this.artifactResolver = artifactResolver;
090            this.kernel = kernel;
091    
092            // Create and start the reaper...
093            this.reaper = new DeployerReaper(REAPER_INTERVAL);
094            Thread t = new Thread(reaper, "Geronimo Config Store Reaper");
095            t.setDaemon(true);
096            t.start();
097        }
098    
099        public List deploy(boolean inPlace, File moduleFile, File planFile) throws DeploymentException {
100            return deploy(inPlace, moduleFile, planFile, null);
101        }
102    
103        public List deploy(boolean inPlace, File moduleFile, File planFile, String targetConfigStore) throws DeploymentException {
104            File originalModuleFile = moduleFile;
105            File tmpDir = null;
106            if (moduleFile != null && !moduleFile.isDirectory()) {
107                // todo jar url handling with Sun's VM on Windows leaves a lock on the module file preventing rebuilds
108                // to address this we use a gross hack and copy the file to a temporary directory
109                // the lock on the file will prevent that being deleted properly until the URLJarFile has
110                // been GC'ed.
111                try {
112                    tmpDir = File.createTempFile("geronimo-deployer", ".tmpdir");
113                    tmpDir.delete();
114                    tmpDir.mkdir();
115                    File tmpFile = new File(tmpDir, moduleFile.getName());
116                    DeploymentUtil.copyFile(moduleFile, tmpFile);
117                    moduleFile = tmpFile;
118                } catch (IOException e) {
119                    throw new DeploymentException(e);
120                }
121            }
122    
123            try {
124                return deploy(inPlace, planFile, moduleFile, null, true, null, null, null, null, null, null, null, targetConfigStore);
125            } catch (DeploymentException e) {
126                log.debug("Deployment failed: plan=" + planFile + ", module=" + originalModuleFile, e);
127                throw e.cleanse();
128            } finally {
129                if (tmpDir != null) {
130                    if (!DeploymentUtil.recursiveDelete(tmpDir)) {
131                        pendingDeletionIndex.setProperty(tmpDir.getName(), "delete");
132                    }
133                }
134            }
135        }
136    
137        /**
138         * Gets a URL that a remote deploy client can use to upload files to the
139         * server. Looks up a remote deploy web application by searching for a
140         * particular GBean and figuring out a reference to the web application
141         * based on that.  Then constructs a URL pointing to that web application
142         * based on available connectors for the web container and the context
143         * root for the web application.
144         *
145         * @return The URL that clients should use for deployment file uploads.
146         */
147        public String getRemoteDeployUploadURL() {
148            // Get the token GBean from the remote deployment configuration
149            Set set = kernel.listGBeans(new AbstractNameQuery("org.apache.geronimo.deployment.remote.RemoteDeployToken"));
150            if (set.size() == 0) {
151                return null;
152            }
153            AbstractName token = (AbstractName) set.iterator().next();
154            // Identify the parent configuration for that GBean
155            set = kernel.getDependencyManager().getParents(token);
156            ObjectName config = null;
157            for (Iterator it = set.iterator(); it.hasNext();) {
158                AbstractName name = (AbstractName) it.next();
159                if (Configuration.isConfigurationObjectName(name.getObjectName())) {
160                    config = name.getObjectName();
161                    break;
162                }
163            }
164            if (config == null) {
165                log.warn("Unable to find remote deployment configuration; is the remote deploy web application running?");
166                return null;
167            }
168            // Generate the URL based on the remote deployment configuration
169            Hashtable hash = new Hashtable();
170            hash.put("J2EEApplication", token.getObjectName().getKeyProperty("J2EEApplication"));
171            hash.put("j2eeType", "WebModule");
172            try {
173                hash.put("name", Configuration.getConfigurationID(config).toString());
174                Set names = kernel.listGBeans(new AbstractNameQuery(null, hash));
175                if (names.size() != 1) {
176                    log.error("Unable to look up remote deploy upload URL");
177                    return null;
178                }
179                AbstractName module = (AbstractName) names.iterator().next();
180                return kernel.getAttribute(module, "URLFor") + "/upload";
181            } catch (Exception e) {
182                log.error("Unable to look up remote deploy upload URL", e);
183                return null;
184            }
185        }
186    
187        public List deploy(boolean inPlace,
188                File planFile,
189                File moduleFile,
190                File targetFile,
191                boolean install,
192                String mainClass,
193                String mainGBean, String mainMethod, String manifestConfigurations, String classPath,
194                String endorsedDirs,
195                String extensionDirs,
196                String targetConfigurationStore) throws DeploymentException {
197            if (planFile == null && moduleFile == null) {
198                throw new DeploymentException("No plan or module specified");
199            }
200    
201            if (planFile != null) {
202                if (!planFile.exists()) {
203                    throw new DeploymentException("Plan file does not exist: " + planFile.getAbsolutePath());
204                }
205                if (!planFile.isFile()) {
206                    throw new DeploymentException("Plan file is not a regular file: " + planFile.getAbsolutePath());
207                }
208            }
209    
210            JarFile module = null;
211            if (moduleFile != null) {
212                if (inPlace && !moduleFile.isDirectory()) {
213                    throw new DeploymentException("In place deployment is not allowed for packed module");
214                }
215                if (!moduleFile.exists()) {
216                    throw new DeploymentException("Module file does not exist: " + moduleFile.getAbsolutePath());
217                }
218                try {
219                    module = DeploymentUtil.createJarFile(moduleFile);
220                } catch (IOException e) {
221                    throw new DeploymentException("Cound not open module file: " + moduleFile.getAbsolutePath(), e);
222                }
223            }
224    
225    //        File configurationDir = null;
226            ModuleIDBuilder idBuilder = new ModuleIDBuilder();
227            try {
228                Object plan = null;
229                ConfigurationBuilder builder = null;
230                for (Iterator i = builders.iterator(); i.hasNext();) {
231                    ConfigurationBuilder candidate = (ConfigurationBuilder) i.next();
232                    plan = candidate.getDeploymentPlan(planFile, module, idBuilder);
233                    if (plan != null) {
234                        builder = candidate;
235                        break;
236                    }
237                }
238                if (builder == null) {
239                    throw new DeploymentException("Cannot deploy the requested application module because no deployer is able to handle it. " +
240                            " This can happen if you have omitted the J2EE deployment descriptor, disabled a deployer module, or if, for example, you are trying to deploy an" +
241                            " EJB module on a minimal Geronimo server that does not have EJB support installed.  (" +
242                            (planFile == null ? "" : "planFile=" + planFile.getAbsolutePath()) +
243                            (moduleFile == null ? "" : (planFile == null ? "" : ", ") + "moduleFile=" + moduleFile.getAbsolutePath()) + ")");
244                }
245    
246                Artifact configID = builder.getConfigurationID(plan, module, idBuilder);
247                // If the Config ID isn't fully resolved, populate it with defaults
248                if (!configID.isResolved()) {
249                    configID = idBuilder.resolve(configID, "car");
250                }
251                // Make sure this configuration doesn't already exist
252                try {
253                    kernel.getGBeanState(Configuration.getConfigurationAbstractName(configID));
254                    throw new DeploymentException("Module " + configID + " already exists in the server.  Try to undeploy it first or use the redeploy command.");
255                } catch (GBeanNotFoundException e) {
256                    // this is good
257                }
258    
259                // create the manifest
260                Manifest manifest;
261                if (mainClass != null) {
262                    manifest = new Manifest();
263                    Attributes mainAttributes = manifest.getMainAttributes();
264                    mainAttributes.putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0");
265                    if (mainClass != null) {
266                        mainAttributes.putValue(Attributes.Name.MAIN_CLASS.toString(), mainClass);
267                    }
268                    if (mainGBean != null) {
269                        mainAttributes.putValue(CommandLineManifest.MAIN_GBEAN.toString(), mainGBean);
270                    }
271                    if (mainMethod != null) {
272                        mainAttributes.putValue(CommandLineManifest.MAIN_METHOD.toString(), mainMethod);
273                    }
274                    if (manifestConfigurations != null) {
275                        mainAttributes.putValue(CommandLineManifest.CONFIGURATIONS.toString(), manifestConfigurations);
276                    }
277                    if (classPath != null) {
278                        mainAttributes.putValue(Attributes.Name.CLASS_PATH.toString(), classPath);
279                    }
280                    if (endorsedDirs != null) {
281                        mainAttributes.putValue(CommandLineManifest.ENDORSED_DIRS.toString(), endorsedDirs);
282                    }
283                    if (extensionDirs != null) {
284                        mainAttributes.putValue(CommandLineManifest.EXTENSION_DIRS.toString(), extensionDirs);
285                    }
286                } else {
287                    manifest = null;
288                }
289    
290                if (stores.isEmpty()) {
291                    throw new DeploymentException("No ConfigurationStores!");
292                }
293                ConfigurationStore store;
294                if (targetConfigurationStore != null) {
295                    AbstractName targetStoreName = new AbstractName(new URI(targetConfigurationStore));
296                    store = (ConfigurationStore) kernel.getGBean(targetStoreName);
297                } else {
298                    store = (ConfigurationStore) stores.iterator().next();
299                }
300    
301                // It's our responsibility to close this context, once we're done with it...
302                DeploymentContext context = builder.buildConfiguration(inPlace, configID, plan, module, stores, artifactResolver, store);
303                List configurations = new ArrayList();
304                boolean configsCleanupRequired = false;
305                configurations.add(context.getConfigurationData());
306                configurations.addAll(context.getAdditionalDeployment());
307    
308                if (configurations.isEmpty()) {
309                    throw new DeploymentException("Deployer did not create any configurations");
310                }
311    
312                // Set TCCL to the classloader for the configuration being deployed
313                // so that any static blocks invoked during the loading of classes 
314                // during serialization of the configuration have the correct TCCL 
315                // ( a TCCL that is consistent with what is set when the same
316                // classes are loaded when the configuration is started.
317                Thread thread = Thread.currentThread();
318                ClassLoader oldCl = thread.getContextClassLoader();
319                thread.setContextClassLoader( context.getConfiguration().getConfigurationClassLoader());
320                try {
321                    if (targetFile != null) {
322                        if (configurations.size() > 1) {
323                            throw new DeploymentException("Deployer created more than one configuration");
324                        }
325                        ConfigurationData configurationData = (ConfigurationData) configurations.get(0);
326                        ExecutableConfigurationUtil.createExecutableConfiguration(configurationData, manifest, targetFile);
327                    }
328                    if (install) {
329                        List deployedURIs = new ArrayList();
330                        for (Iterator iterator = configurations.iterator(); iterator.hasNext();) {
331                            ConfigurationData configurationData = (ConfigurationData) iterator.next();
332                            store.install(configurationData);
333                            deployedURIs.add(configurationData.getId().toString());
334                        }
335                        notifyWatchers(deployedURIs);
336                        return deployedURIs;
337                    } else {
338                        configsCleanupRequired = true;
339                        return Collections.EMPTY_LIST;
340                    }
341                } catch (DeploymentException e) {
342                    configsCleanupRequired = true;
343                    throw e;
344                } catch (IOException e) {
345                    configsCleanupRequired = true;
346                    throw e;
347                } catch (InvalidConfigException e) {
348                    configsCleanupRequired = true;
349                    // unlikely as we just built this
350                    throw new DeploymentException(e);
351                } catch (Throwable e) {
352                    // Could get here if serialization of the configuration failed (GERONIMO-1996)
353                    configsCleanupRequired = true;
354                    throw e;
355                } finally {
356                    thread.setContextClassLoader(oldCl);
357                    if (context != null) {
358                        context.close();
359                    }
360                    if (configsCleanupRequired) {
361                        // We do this after context is closed so the module jar isn't open
362                        cleanupConfigurations(configurations);
363                    }
364                }
365            } catch (Throwable e) {
366                //TODO not clear all errors will result in total cleanup
367    //            File configurationDir = configurationData.getConfigurationDir();
368    //            if (!DeploymentUtil.recursiveDelete(configurationDir)) {
369    //                pendingDeletionIndex.setProperty(configurationDir.getName(), new String("delete"));
370    //                log.debug("Queued deployment directory to be reaped " + configurationDir);
371    //            }
372    //            if (targetFile != null) {
373    //                targetFile.delete();
374    //            }
375    
376                if (e instanceof Error) {
377                    log.error("Deployment failed due to ", e);
378                    throw (Error) e;
379                } else if (e instanceof DeploymentException) {
380                    throw (DeploymentException) e;
381                } else if (e instanceof Exception) {
382                    log.error("Deployment failed due to ", e);
383                    throw new DeploymentException(e);
384                }
385                throw new Error(e);
386            } finally {
387                DeploymentUtil.close(module);
388            }
389        }
390    
391        private void notifyWatchers(List list) {
392            Artifact[] arts = new Artifact[list.size()];
393            for (int i = 0; i < list.size(); i++) {
394                String s = (String) list.get(i);
395                arts[i] = Artifact.create(s);
396            }
397            for (Iterator it = watchers.iterator(); it.hasNext();) {
398                DeploymentWatcher watcher = (DeploymentWatcher) it.next();
399                for (int i = 0; i < arts.length; i++) {
400                    Artifact art = arts[i];
401                    watcher.deployed(art);
402                }
403            }
404        }
405    
406        private void cleanupConfigurations(List configurations) {
407            for (Iterator iterator = configurations.iterator(); iterator.hasNext();) {
408                ConfigurationData configurationData = (ConfigurationData) iterator.next();
409                File configurationDir = configurationData.getConfigurationDir();
410                if (!DeploymentUtil.recursiveDelete(configurationDir)) {
411                    pendingDeletionIndex.setProperty(configurationDir.getName(), "delete");
412                    log.debug("Queued deployment directory to be reaped " + configurationDir);
413                }
414            }
415        }
416    
417        /**
418         * Thread to cleanup unused temporary Deployer directories (and files).
419         * On Windows, open files can't be deleted. Until MultiParentClassLoaders
420         * are GC'ed, we won't be able to delete Config Store directories/files.
421         */
422        class DeployerReaper implements Runnable {
423            private final int reaperInterval;
424            private volatile boolean done = false;
425    
426            public DeployerReaper(int reaperInterval) {
427                this.reaperInterval = reaperInterval;
428            }
429    
430            public void close() {
431                this.done = true;
432            }
433    
434            public void run() {
435                log.debug("ConfigStoreReaper started");
436                while (!done) {
437                    try {
438                        Thread.sleep(reaperInterval);
439                    } catch (InterruptedException e) {
440                        continue;
441                    }
442                    reap();
443                }
444            }
445    
446            /**
447             * For every directory in the pendingDeletionIndex, attempt to delete all
448             * sub-directories and files.
449             */
450            public void reap() {
451                // return, if there's nothing to do
452                if (pendingDeletionIndex.size() == 0)
453                    return;
454                // Otherwise, attempt to delete all of the directories
455                Enumeration list = pendingDeletionIndex.propertyNames();
456                while (list.hasMoreElements()) {
457                    String dirName = (String) list.nextElement();
458                    File deleteDir = new File(dirName);
459    
460                    if (!DeploymentUtil.recursiveDelete(deleteDir)) {
461                        pendingDeletionIndex.remove(deleteDir);
462                        log.debug("Reaped deployment directory " + deleteDir);
463                    }
464                }
465            }
466        }
467    
468        public static final GBeanInfo GBEAN_INFO;
469    
470        private static final String DEPLOYER = "Deployer";
471    
472        static {
473            GBeanInfoBuilder infoFactory = GBeanInfoBuilder.createStatic(Deployer.class, DEPLOYER);
474    
475            infoFactory.addAttribute("kernel", Kernel.class, false);
476            infoFactory.addAttribute("remoteDeployUploadURL", String.class, false);
477            infoFactory.addOperation("deploy", new Class[]{boolean.class, File.class, File.class});
478            infoFactory.addOperation("deploy", new Class[]{boolean.class, File.class, File.class, String.class});
479            infoFactory.addOperation("deploy", new Class[]{boolean.class, File.class, File.class, File.class, boolean.class, String.class, String.class, String.class, String.class, String.class, String.class, String.class, String.class});
480    
481            infoFactory.addReference("Builders", ConfigurationBuilder.class, "ConfigBuilder");
482            infoFactory.addReference("Store", ConfigurationStore.class, "ConfigurationStore");
483            infoFactory.addReference("Watchers", DeploymentWatcher.class);
484    
485            infoFactory.setConstructor(new String[]{"Builders", "Store", "Watchers", "kernel"});
486    
487            GBEAN_INFO = infoFactory.getBeanInfo();
488        }
489    
490        public static GBeanInfo getGBeanInfo() {
491            return GBEAN_INFO;
492        }
493    }