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