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.FilenameFilter;
022    import java.io.IOException;
023    import java.net.URI;
024    import java.net.URISyntaxException;
025    import java.util.ArrayList;
026    import java.util.Collection;
027    import java.util.Collections;
028    import java.util.Enumeration;
029    import java.util.Hashtable;
030    import java.util.Iterator;
031    import java.util.List;
032    import java.util.Properties;
033    import java.util.Set;
034    import java.util.jar.Attributes;
035    import java.util.jar.JarFile;
036    import java.util.jar.Manifest;
037    
038    import javax.management.ObjectName;
039    
040    import org.apache.commons.logging.Log;
041    import org.apache.commons.logging.LogFactory;
042    import org.apache.geronimo.common.DeploymentException;
043    import org.apache.geronimo.deployment.util.DeploymentUtil;
044    import org.apache.geronimo.gbean.AbstractName;
045    import org.apache.geronimo.gbean.AbstractNameQuery;
046    import org.apache.geronimo.gbean.GBeanInfo;
047    import org.apache.geronimo.gbean.GBeanInfoBuilder;
048    import org.apache.geronimo.gbean.GBeanLifecycle;
049    import org.apache.geronimo.kernel.GBeanNotFoundException;
050    import org.apache.geronimo.kernel.Kernel;
051    import org.apache.geronimo.kernel.config.Configuration;
052    import org.apache.geronimo.kernel.config.ConfigurationData;
053    import org.apache.geronimo.kernel.config.ConfigurationManager;
054    import org.apache.geronimo.kernel.config.ConfigurationStore;
055    import org.apache.geronimo.kernel.config.ConfigurationUtil;
056    import org.apache.geronimo.kernel.config.InvalidConfigException;
057    import org.apache.geronimo.kernel.config.DeploymentWatcher;
058    import org.apache.geronimo.kernel.repository.Artifact;
059    import org.apache.geronimo.kernel.repository.ArtifactResolver;
060    import org.apache.geronimo.system.configuration.ExecutableConfigurationUtil;
061    import org.apache.geronimo.system.main.CommandLineManifest;
062    
063    /**
064     * GBean that knows how to deploy modules (by consulting available module builders)
065     *
066     * @version $Rev: 706640 $ $Date: 2008-10-21 14:44:05 +0000 (Tue, 21 Oct 2008) $
067     */
068    public class Deployer implements GBeanLifecycle {
069        private static final Log log = LogFactory.getLog(Deployer.class);
070        private final int REAPER_INTERVAL = 60 * 1000;    
071        private DeployerReaper reaper;
072        private final String remoteDeployAddress;
073        private final Collection builders;
074        private final Collection stores;
075        private final Collection watchers;
076        private final ArtifactResolver artifactResolver;
077        private final Kernel kernel;
078    
079        public Deployer(String remoteDeployAddress, Collection builders, Collection stores, Collection watchers, Kernel kernel) {
080            this(remoteDeployAddress, builders, stores, watchers, getArtifactResolver(kernel), kernel);
081        }
082    
083        private static ArtifactResolver getArtifactResolver(Kernel kernel) {
084            ConfigurationManager configurationManager = ConfigurationUtil.getConfigurationManager(kernel);
085            return configurationManager.getArtifactResolver();
086        }
087    
088        public Deployer(String remoteDeployAddress, Collection builders, Collection stores, Collection watchers, ArtifactResolver artifactResolver, Kernel kernel) {
089            this.remoteDeployAddress = remoteDeployAddress;
090            this.builders = builders;
091            this.stores = stores;
092            this.watchers = watchers;
093            this.artifactResolver = artifactResolver;
094            this.kernel = kernel;
095    
096            // Create and start the reaper...
097            this.reaper = new DeployerReaper(REAPER_INTERVAL);
098    
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                boolean cleanup = true;
114                try {
115                    tmpDir = File.createTempFile("geronimo-deployer", ".tmpdir");
116                    tmpDir.delete();
117                    tmpDir.mkdir();
118                    File tmpFile = new File(tmpDir, moduleFile.getName());
119                    DeploymentUtil.copyFile(moduleFile, tmpFile);
120                    moduleFile = tmpFile;
121                    cleanup = false;
122                } catch (IOException e) {
123                    throw new DeploymentException(e);
124                } finally {
125                    // If an Exception is thrown in the try block above, we will need to cleanup here. 
126                    if(cleanup && tmpDir != null && !DeploymentUtil.recursiveDelete(tmpDir)) {
127                        reaper.delete(tmpDir.getAbsolutePath(), "delete");
128                    }
129                }
130            }
131    
132            try {
133                return deploy(inPlace, moduleFile, planFile, null, true, null, null, null, null, null, null, null, targetConfigStore);
134            } catch (DeploymentException e) {
135                log.debug("Deployment failed: plan=" + planFile + ", module=" + originalModuleFile, e);
136                throw e.cleanse();
137            } finally {
138                if (tmpDir != null) {
139                    if (!DeploymentUtil.recursiveDelete(tmpDir)) {
140                        reaper.delete(tmpDir.getAbsolutePath(), "delete");
141                    }
142                }
143            }
144        }
145    
146        /**
147         * Gets a URL that a remote deploy client can use to upload files to the
148         * server. Looks up a remote deploy web application by searching for a
149         * particular GBean and figuring out a reference to the web application
150         * based on that.  Then constructs a URL pointing to that web application
151         * based on available connectors for the web container and the context
152         * root for the web application.
153         *
154         * @return The URL that clients should use for deployment file uploads.
155         */
156        public String getRemoteDeployUploadURL() {
157            // Get the token GBean from the remote deployment configuration
158            Set set = kernel.listGBeans(new AbstractNameQuery("org.apache.geronimo.deployment.remote.RemoteDeployToken"));
159            if (set.size() == 0) {
160                return null;
161            }
162            AbstractName token = (AbstractName) set.iterator().next();
163            // Identify the parent configuration for that GBean
164            set = kernel.getDependencyManager().getParents(token);
165            ObjectName config = null;
166            for (Iterator it = set.iterator(); it.hasNext();) {
167                AbstractName name = (AbstractName) it.next();
168                if (Configuration.isConfigurationObjectName(name.getObjectName())) {
169                    config = name.getObjectName();
170                    break;
171                }
172            }
173            if (config == null) {
174                log.warn("Unable to find remote deployment configuration; is the remote deploy web application running?");
175                return null;
176            }
177            // Generate the URL based on the remote deployment configuration
178            Hashtable hash = new Hashtable();
179            hash.put("J2EEApplication", token.getObjectName().getKeyProperty("J2EEApplication"));
180            hash.put("j2eeType", "WebModule");
181            try {
182                hash.put("name", Configuration.getConfigurationID(config).toString());
183                Set names = kernel.listGBeans(new AbstractNameQuery(null, hash));
184                if (names.size() != 1) {
185                    log.error("Unable to look up remote deploy upload URL");
186                    return null;
187                }
188                AbstractName module = (AbstractName) names.iterator().next();
189                String contextPath = (String) kernel.getAttribute(module, "contextPath");
190                if (null == contextPath) {
191                    throw new IllegalStateException("Cannot find contextPath attribute for [" + module + "]");
192                }
193                String temp = remoteDeployAddress + "/" + contextPath + "/upload";
194                return URI.create(temp).normalize().toString();
195            } catch (Exception e) {
196                log.error("Unable to look up remote deploy upload URL", e);
197                return null;
198            }
199        }
200    
201        public List deploy(boolean inPlace,
202                File moduleFile,
203                File planFile,
204                File targetFile,
205                boolean install,
206                String mainClass,
207                String mainGBean, String mainMethod, String manifestConfigurations, String classPath,
208                String endorsedDirs,
209                String extensionDirs,
210                String targetConfigurationStore) throws DeploymentException {
211            if (planFile == null && moduleFile == null) {
212                throw new DeploymentException("No plan or module specified");
213            } else if (stores.isEmpty()) {
214                throw new DeploymentException("No ConfigurationStores!");
215            }
216            validatePlanFile(planFile);
217    
218            JarFile module = getModule(inPlace, moduleFile);
219    
220            ModuleIDBuilder idBuilder = new ModuleIDBuilder();
221            try {
222                Object plan = null;
223                ConfigurationBuilder builder = null;
224                for (Iterator i = builders.iterator(); i.hasNext();) {
225                    ConfigurationBuilder candidate = (ConfigurationBuilder) i.next();
226                    plan = candidate.getDeploymentPlan(planFile, module, idBuilder);
227                    if (plan != null) {
228                        builder = candidate;
229                        break;
230                    }
231                }
232                if (builder == null) {
233                    throw new DeploymentException("Cannot deploy the requested application module because no deployer is able to handle it. " +
234                            " 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" +
235                            " EJB module on a minimal Geronimo server that does not have EJB support installed.  (" +
236                            (planFile == null ? "" : "planFile=" + planFile.getAbsolutePath()) +
237                            (moduleFile == null ? "" : (planFile == null ? "" : ", ") + "moduleFile=" + moduleFile.getAbsolutePath()) + ")");
238                }
239    
240                Artifact configID = getConfigID(module, idBuilder, plan, builder);
241    
242                // create the manifest
243                Manifest manifest = createManifest(mainClass,
244                    mainGBean,
245                    mainMethod,
246                    manifestConfigurations,
247                    classPath,
248                    endorsedDirs,
249                    extensionDirs);
250    
251                ConfigurationStore store = getConfigurationStore(targetConfigurationStore);
252    
253                // It's our responsibility to close this context, once we're done with it...
254                DeploymentContext context = builder.buildConfiguration(inPlace, configID, plan, module, stores, artifactResolver, store);
255                
256                return install(targetFile, install, manifest, store, context);
257            } catch (Throwable e) {
258                //TODO not clear all errors will result in total cleanup
259    //            File configurationDir = configurationData.getConfigurationDir();
260    //            if (!DeploymentUtil.recursiveDelete(configurationDir)) {
261    //                pendingDeletionIndex.setProperty(configurationDir.getName(), new String("delete"));
262    //                log.debug("Queued deployment directory to be reaped " + configurationDir);
263    //            }
264    //            if (targetFile != null) {
265    //                targetFile.delete();
266    //            }
267    
268                if (e instanceof Error) {
269                    log.error("Deployment failed due to ", e);
270                    throw (Error) e;
271                } else if (e instanceof DeploymentException) {
272                    throw (DeploymentException) e;
273                } else if (e instanceof Exception) {
274                    log.error("Deployment failed due to ", e);
275                    throw new DeploymentException(e);
276                }
277                throw new Error(e);
278            } finally {
279                DeploymentUtil.close(module);
280            }
281        }
282    
283        private ConfigurationStore getConfigurationStore(String targetConfigurationStore) 
284                throws URISyntaxException, GBeanNotFoundException {
285            if (targetConfigurationStore != null) {
286                AbstractName targetStoreName = new AbstractName(new URI(targetConfigurationStore));
287                return (ConfigurationStore) kernel.getGBean(targetStoreName);
288            } else {
289                return (ConfigurationStore) stores.iterator().next();
290            }
291        }
292    
293        private Artifact getConfigID(JarFile module, 
294                ModuleIDBuilder idBuilder, 
295                Object plan, 
296                ConfigurationBuilder builder) throws IOException, DeploymentException, InvalidConfigException {
297            Artifact configID = builder.getConfigurationID(plan, module, idBuilder);
298            // If the Config ID isn't fully resolved, populate it with defaults
299            if (!configID.isResolved()) {
300                configID = idBuilder.resolve(configID, "car");
301            }
302            
303            // Make sure this configuration doesn't already exist
304            try {
305                kernel.getGBeanState(Configuration.getConfigurationAbstractName(configID));
306                throw new DeploymentException("Module " + configID + " already exists in the server.  Try to undeploy it first or use the redeploy command.");
307            } catch (GBeanNotFoundException e) {
308                // this is good
309            }
310            return configID;
311        }
312    
313        private List install(File targetFile,
314                boolean install,
315                Manifest manifest,
316                ConfigurationStore store,
317                DeploymentContext context) throws DeploymentException, IOException, Throwable {
318            List<ConfigurationData> configurations = new ArrayList<ConfigurationData>();
319            configurations.add(context.getConfigurationData());
320            configurations.addAll(context.getAdditionalDeployment());
321    
322            if (configurations.isEmpty()) {
323                throw new DeploymentException("Deployer did not create any configurations");
324            }
325    
326            boolean configsCleanupRequired = false;
327    
328            // Set TCCL to the classloader for the configuration being deployed
329            // so that any static blocks invoked during the loading of classes 
330            // during serialization of the configuration have the correct TCCL 
331            // ( a TCCL that is consistent with what is set when the same
332            // classes are loaded when the configuration is started.
333            Thread thread = Thread.currentThread();
334            ClassLoader oldCl = thread.getContextClassLoader();
335            thread.setContextClassLoader( context.getConfiguration().getConfigurationClassLoader());
336            try {
337                if (targetFile != null) {
338                    if (configurations.size() > 1) {
339                        throw new DeploymentException("Deployer created more than one configuration");
340                    }
341                    ConfigurationData configurationData = (ConfigurationData) configurations.get(0);
342                    ExecutableConfigurationUtil.createExecutableConfiguration(configurationData, manifest, targetFile);
343                }
344                if (install) {
345                    List deployedURIs = new ArrayList();
346                    for (Iterator iterator = configurations.iterator(); iterator.hasNext();) {
347                        ConfigurationData configurationData = (ConfigurationData) iterator.next();
348                        store.install(configurationData);
349                        deployedURIs.add(configurationData.getId().toString());
350                    }
351                    notifyWatchers(deployedURIs);
352                    return deployedURIs;
353                } else {
354                    configsCleanupRequired = true;
355                    return Collections.EMPTY_LIST;
356                }
357            } catch (DeploymentException e) {
358                configsCleanupRequired = true;
359                throw e;
360            } catch (IOException e) {
361                configsCleanupRequired = true;
362                throw e;
363            } catch (InvalidConfigException e) {
364                configsCleanupRequired = true;
365                // unlikely as we just built this
366                throw new DeploymentException(e);
367            } catch (Throwable e) {
368                // Could get here if serialization of the configuration failed (GERONIMO-1996)
369                configsCleanupRequired = true;
370                throw e;
371            } finally {
372                thread.setContextClassLoader(oldCl);
373                if (context != null) {
374                    context.close();
375                }
376                if (configsCleanupRequired) {
377                    // We do this after context is closed so the module jar isn't open
378                    cleanupConfigurations(configurations);
379                }
380            }
381        }
382    
383        private Manifest createManifest(String mainClass,
384                String mainGBean,
385                String mainMethod,
386                String manifestConfigurations,
387                String classPath,
388                String endorsedDirs,
389                String extensionDirs) {
390            if (mainClass == null) {
391                return null;
392            }
393            Manifest manifest = new Manifest();
394            Attributes mainAttributes = manifest.getMainAttributes();
395            mainAttributes.putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0");
396            if (mainClass != null) {
397                mainAttributes.putValue(Attributes.Name.MAIN_CLASS.toString(), mainClass);
398            }
399            if (mainGBean != null) {
400                mainAttributes.putValue(CommandLineManifest.MAIN_GBEAN.toString(), mainGBean);
401            }
402            if (mainMethod != null) {
403                mainAttributes.putValue(CommandLineManifest.MAIN_METHOD.toString(), mainMethod);
404            }
405            if (manifestConfigurations != null) {
406                mainAttributes.putValue(CommandLineManifest.CONFIGURATIONS.toString(), manifestConfigurations);
407            }
408            if (classPath != null) {
409                mainAttributes.putValue(Attributes.Name.CLASS_PATH.toString(), classPath);
410            }
411            if (endorsedDirs != null) {
412                mainAttributes.putValue(CommandLineManifest.ENDORSED_DIRS.toString(), endorsedDirs);
413            }
414            if (extensionDirs != null) {
415                mainAttributes.putValue(CommandLineManifest.EXTENSION_DIRS.toString(), extensionDirs);
416            }
417            return manifest;
418        }
419    
420        private JarFile getModule(boolean inPlace, File moduleFile) throws DeploymentException {
421            JarFile module = null;
422            if (moduleFile != null) {
423                if (inPlace && !moduleFile.isDirectory()) {
424                    throw new DeploymentException("In place deployment is not allowed for packed module");
425                }
426                if (!moduleFile.exists()) {
427                    throw new DeploymentException("Module file does not exist: " + moduleFile.getAbsolutePath());
428                }
429                try {
430                    module = DeploymentUtil.createJarFile(moduleFile);
431                } catch (IOException e) {
432                    throw new DeploymentException("Cound not open module file: " + moduleFile.getAbsolutePath(), e);
433                }
434            }
435            return module;
436        }
437    
438        private void validatePlanFile(File planFile) throws DeploymentException {
439            if (planFile != null) {
440                if (!planFile.exists()) {
441                    throw new DeploymentException("Plan file does not exist: " + planFile.getAbsolutePath());
442                }
443                if (!planFile.isFile()) {
444                    throw new DeploymentException("Plan file is not a regular file: " + planFile.getAbsolutePath());
445                }
446            }
447        }
448    
449        private void notifyWatchers(List list) {
450            Artifact[] arts = new Artifact[list.size()];
451            for (int i = 0; i < list.size(); i++) {
452                String s = (String) list.get(i);
453                arts[i] = Artifact.create(s);
454            }
455            for (Iterator it = watchers.iterator(); it.hasNext();) {
456                DeploymentWatcher watcher = (DeploymentWatcher) it.next();
457                for (int i = 0; i < arts.length; i++) {
458                    Artifact art = arts[i];
459                    watcher.deployed(art);
460                }
461            }
462        }
463    
464        private void cleanupConfigurations(List configurations) {
465            for (Iterator iterator = configurations.iterator(); iterator.hasNext();) {
466                ConfigurationData configurationData = (ConfigurationData) iterator.next();
467                File configurationDir = configurationData.getConfigurationDir();
468                if (!DeploymentUtil.recursiveDelete(configurationDir)) {
469                    reaper.delete(configurationDir.getAbsolutePath(), "delete");
470                }
471            }
472        }
473        
474        public void doStart() throws Exception {
475        }
476        
477        public void doFail() {
478            if (reaper != null) {
479                reaper.close();
480            }
481        }
482    
483        public void doStop() throws Exception {
484        }
485    
486        /**
487         * Thread to cleanup unused temporary Deployer directories (and files).
488         * On Windows, open files can't be deleted. Until MultiParentClassLoaders
489         * are GC'ed, we won't be able to delete Config Store directories/files.
490         */
491        class DeployerReaper implements Runnable {
492            private final int reaperInterval;
493            private final Properties pendingDeletionIndex = new Properties();
494            private volatile boolean done = false;
495            private Thread thread;
496    
497            public DeployerReaper(int reaperInterval) {
498                this.reaperInterval = reaperInterval;
499            }
500    
501            public void delete(String dir, String type) {
502                pendingDeletionIndex.setProperty(dir, type);
503                log.debug("Queued deployment directory to be reaped " + dir);
504                startThread();
505            }
506            
507            private synchronized void startThread() {
508                if (this.thread == null) {
509                    this.thread = new Thread(this, "Geronimo Config Store Reaper");
510                    this.thread.setDaemon(true);
511                    this.thread.start();
512                }
513            }
514            
515            public void close() {
516                this.done = true;
517            }
518    
519            public void run() {
520                log.debug("ConfigStoreReaper started");
521    
522                // DeployerReaper in offline deployment does not get a chance to reap the temporary
523                // directories. Reap any temporary directories left behind by previous runs here.
524                reapBacklog();
525    
526                while (!done) {
527                    try {
528                        Thread.sleep(reaperInterval);
529                    } catch (InterruptedException e) {
530                        continue;
531                    }
532                    reap();
533                }
534            }
535    
536            /**
537             * Reap any temporary directories left behind by previous runs.
538             */
539            private void reapBacklog() {
540                try {
541                    File tempFile = File.createTempFile("geronimo-deployer", ".tmpdir");
542                    File tempDir = tempFile.getParentFile();
543                    tempFile.delete();
544                    String[] backlog = tempDir.list(new FilenameFilter(){
545                        public boolean accept(File dir, String name) {
546                            return name.startsWith("geronimo-deployer") && name.endsWith(".tmpdir") && new File(dir, name).isDirectory();
547                        }});
548                    for(String dir: backlog) {
549                        File deleteDir = new File(tempDir, dir);
550                        DeploymentUtil.recursiveDelete(deleteDir);
551                        log.debug("Reaped deployment directory from previous runs " + deleteDir);
552                    }
553                } catch (IOException ignored) {
554                }
555            }
556    
557            /**
558             * For every directory in the pendingDeletionIndex, attempt to delete all
559             * sub-directories and files.
560             */
561            public void reap() {
562                // return, if there's nothing to do
563                if (pendingDeletionIndex.size() == 0)
564                    return;
565                // Otherwise, attempt to delete all of the directories
566                Enumeration list = pendingDeletionIndex.propertyNames();
567                while (list.hasMoreElements()) {
568                    String dirName = (String) list.nextElement();
569                    File deleteDir = new File(dirName);
570    
571                    if (DeploymentUtil.recursiveDelete(deleteDir)) {
572                        pendingDeletionIndex.remove(dirName);
573                        log.debug("Reaped deployment directory " + deleteDir);
574                    }
575                }
576            }
577        }
578    
579        public static final GBeanInfo GBEAN_INFO;
580    
581        private static final String DEPLOYER = "Deployer";
582    
583        static {
584            GBeanInfoBuilder infoFactory = GBeanInfoBuilder.createStatic(Deployer.class, DEPLOYER);
585    
586            infoFactory.addAttribute("kernel", Kernel.class, false);
587            infoFactory.addAttribute("remoteDeployAddress", String.class, true, true);
588            infoFactory.addAttribute("remoteDeployUploadURL", String.class, false);
589            infoFactory.addOperation("deploy", new Class[]{boolean.class, File.class, File.class});
590            infoFactory.addOperation("deploy", new Class[]{boolean.class, File.class, File.class, String.class});
591            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});
592    
593            infoFactory.addReference("Builders", ConfigurationBuilder.class, "ConfigBuilder");
594            infoFactory.addReference("Store", ConfigurationStore.class, "ConfigurationStore");
595            infoFactory.addReference("Watchers", DeploymentWatcher.class);
596    
597            infoFactory.setConstructor(new String[]{"remoteDeployAddress", "Builders", "Store", "Watchers", "kernel"});
598    
599            GBEAN_INFO = infoFactory.getBeanInfo();
600        }
601    
602        public static GBeanInfo getGBeanInfo() {
603            return GBEAN_INFO;
604        }
605    
606    }