View Javadoc

1   /*
2    * $Id: DefaultMuleApplication.java 22568 2011-07-28 18:52:55Z julien.eluard $
3    * --------------------------------------------------------------------------------------
4    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
5    *
6    * The software in this package is published under the terms of the CPAL v1.0
7    * license, a copy of which has been included with this distribution in the
8    * LICENSE.txt file.
9    */
10  
11  package org.mule.module.launcher.application;
12  
13  import org.mule.MuleServer;
14  import org.mule.api.MuleContext;
15  import org.mule.api.MuleException;
16  import org.mule.api.config.ConfigurationBuilder;
17  import org.mule.api.config.MuleProperties;
18  import org.mule.api.context.notification.MuleContextNotificationListener;
19  import org.mule.config.builders.AutoConfigurationBuilder;
20  import org.mule.config.builders.SimpleConfigurationBuilder;
21  import org.mule.config.i18n.CoreMessages;
22  import org.mule.config.i18n.MessageFactory;
23  import org.mule.context.DefaultMuleContextFactory;
24  import org.mule.context.notification.MuleContextNotification;
25  import org.mule.context.notification.NotificationException;
26  import org.mule.module.launcher.AbstractFileWatcher;
27  import org.mule.module.launcher.ApplicationMuleContextBuilder;
28  import org.mule.module.launcher.ConfigChangeMonitorThreadFactory;
29  import org.mule.module.launcher.DefaultMuleSharedDomainClassLoader;
30  import org.mule.module.launcher.DeploymentInitException;
31  import org.mule.module.launcher.DeploymentService;
32  import org.mule.module.launcher.DeploymentStartException;
33  import org.mule.module.launcher.DeploymentStopException;
34  import org.mule.module.launcher.GoodCitizenClassLoader;
35  import org.mule.module.launcher.InstallException;
36  import org.mule.module.launcher.MuleApplicationClassLoader;
37  import org.mule.module.launcher.MuleSharedDomainClassLoader;
38  import org.mule.module.launcher.descriptor.ApplicationDescriptor;
39  import org.mule.module.launcher.plugin.MulePluginsClassLoader;
40  import org.mule.module.launcher.plugin.PluginDescriptor;
41  import org.mule.module.reboot.MuleContainerBootstrapUtils;
42  import org.mule.util.ClassUtils;
43  import org.mule.util.ExceptionUtils;
44  import org.mule.util.FileUtils;
45  import org.mule.util.StringUtils;
46  
47  import java.io.File;
48  import java.io.IOException;
49  import java.util.ArrayList;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.Set;
53  import java.util.concurrent.Executors;
54  import java.util.concurrent.ScheduledExecutorService;
55  import java.util.concurrent.TimeUnit;
56  
57  import org.apache.commons.logging.Log;
58  import org.apache.commons.logging.LogFactory;
59  
60  import static org.mule.util.SplashScreen.miniSplash;
61  
62  public class DefaultMuleApplication implements Application
63  {
64  
65      protected static final int DEFAULT_RELOAD_CHECK_INTERVAL_MS = 3000;
66      protected static final String ANCHOR_FILE_BLURB = "Delete this file while Mule is running to undeploy this app in a clean way.";
67  
68      protected transient final Log logger = LogFactory.getLog(getClass());
69      protected transient final Log deployLogger = LogFactory.getLog(DeploymentService.class);
70  
71      protected ScheduledExecutorService watchTimer;
72  
73      private MuleContext muleContext;
74      private ClassLoader deploymentClassLoader;
75      private ApplicationDescriptor descriptor;
76  
77      protected String[] absoluteResourcePaths;
78  
79      protected DeploymentService deploymentService;
80  
81      protected DefaultMuleApplication(ApplicationDescriptor appDesc)
82      {
83          this.descriptor = appDesc;
84      }
85  
86      public void setDeploymentService(DeploymentService deploymentService)
87      {
88          this.deploymentService = deploymentService;
89      }
90  
91      @Override
92      public void install()
93      {
94          if (logger.isInfoEnabled())
95          {
96              logger.info(miniSplash(String.format("New app '%s'", descriptor.getAppName())));
97          }
98  
99          // convert to absolute paths
100         final String[] configResources = descriptor.getConfigResources();
101         absoluteResourcePaths = new String[configResources.length];
102         for (int i = 0; i < configResources.length; i++)
103         {
104             String resource = configResources[i];
105             final File file = toAbsoluteFile(resource);
106             if (!file.exists())
107             {
108                 throw new InstallException(
109                         MessageFactory.createStaticMessage(String.format("Config for app '%s' not found: %s", getAppName(), file))
110                 );
111             }
112 
113             absoluteResourcePaths[i] = file.getAbsolutePath();
114         }
115 
116         createDeploymentClassLoader();
117     }
118 
119     @Override
120     public String getAppName()
121     {
122         return descriptor.getAppName();
123     }
124 
125     @Override
126     public ApplicationDescriptor getDescriptor()
127     {
128         return descriptor;
129     }
130 
131     public void setAppName(String appName)
132     {
133         this.descriptor.setAppName(appName);
134     }
135 
136     @Override
137     public void start()
138     {
139         if (logger.isInfoEnabled())
140         {
141             logger.info(miniSplash(String.format("Starting app '%s'", descriptor.getAppName())));
142         }
143 
144         try
145         {
146             this.muleContext.start();
147             // save app's state in the marker file
148             File marker = new File(MuleContainerBootstrapUtils.getMuleAppsDir(), String.format("%s-anchor.txt", getAppName()));
149             FileUtils.writeStringToFile(marker, ANCHOR_FILE_BLURB);
150 
151             // null CCL ensures we log at 'system' level
152             // TODO create a more usable wrapper for any logger to be logged at sys level
153             final ClassLoader oldCl = Thread.currentThread().getContextClassLoader();
154             try
155             {
156                 Thread.currentThread().setContextClassLoader(null);
157                 deployLogger.info(miniSplash(String.format("Started app '%s'", descriptor.getAppName())));
158             }
159             finally
160             {
161                 Thread.currentThread().setContextClassLoader(oldCl);
162             }
163         }
164         catch (MuleException e)
165         {
166             // log it here so it ends up in app log, sys log will only log a message without stacktrace
167             logger.error(null, ExceptionUtils.getRootCause(e));
168             // TODO add app name to the exception field
169             throw new DeploymentStartException(CoreMessages.createStaticMessage(ExceptionUtils.getRootCauseMessage(e)), e);
170         }
171         catch (IOException e)
172         {
173             // log it here so it ends up in app log, sys log will only log a message without stacktrace
174             logger.error(null, ExceptionUtils.getRootCause(e));
175             // TODO add app name to the exception field
176             throw new DeploymentStartException(CoreMessages.createStaticMessage(ExceptionUtils.getRootCauseMessage(e)), e);
177         }
178     }
179 
180     @Override
181     public void init()
182     {
183         if (logger.isInfoEnabled())
184         {
185             logger.info(miniSplash(String.format("Initializing app '%s'", descriptor.getAppName())));
186         }
187 
188         try
189         {
190             ConfigurationBuilder cfgBuilder = createConfigurationBuilder();
191             if (!cfgBuilder.isConfigured())
192             {
193                 List<ConfigurationBuilder> builders = new ArrayList<ConfigurationBuilder>(3);
194                 builders.add(createConfigurationBuilderFromApplicationProperties());
195 
196                 // We need to add this builder before spring so that we can use Mule annotations in Spring or any other builder
197                 addAnnotationsConfigBuilderIfPresent(builders);
198                 addIBeansConfigurationBuilderIfPackagesConfiguredForScanning(builders);
199 
200                 builders.add(cfgBuilder);
201 
202                 DefaultMuleContextFactory muleContextFactory = new DefaultMuleContextFactory();
203                 this.muleContext = muleContextFactory.createMuleContext(builders, new ApplicationMuleContextBuilder(descriptor));
204 
205                 if (descriptor.isRedeploymentEnabled())
206                 {
207                     createRedeployMonitor();
208                 }
209             }
210         }
211         catch (Exception e)
212         {
213             // log it here so it ends up in app log, sys log will only log a message without stacktrace
214             logger.error(null, ExceptionUtils.getRootCause(e));
215             throw new DeploymentInitException(CoreMessages.createStaticMessage(ExceptionUtils.getRootCauseMessage(e)), e);
216         }
217     }
218 
219     protected ConfigurationBuilder createConfigurationBuilder() throws Exception
220     {
221         String configBuilderClassName = determineConfigBuilderClassName();
222         return (ConfigurationBuilder) ClassUtils.instanciateClass(configBuilderClassName,
223             new Object[] { absoluteResourcePaths }, getDeploymentClassLoader());
224     }
225 
226     protected String determineConfigBuilderClassName()
227     {
228         // Provide a shortcut for Spring: "-builder spring"
229         final String builderFromDesc = descriptor.getConfigurationBuilder();
230         if ("spring".equalsIgnoreCase(builderFromDesc))
231         {
232             return ApplicationDescriptor.CLASSNAME_SPRING_CONFIG_BUILDER;
233         }
234         else if (builderFromDesc == null)
235         {
236             return AutoConfigurationBuilder.class.getName();
237         }
238         else
239         {
240             return builderFromDesc;
241         }
242     }
243 
244     protected ConfigurationBuilder createConfigurationBuilderFromApplicationProperties()
245     {
246         // Load application properties first since they may be needed by other configuration builders
247         final Map<String,String> appProperties = descriptor.getAppProperties();
248 
249         // Add the app.home variable to the context
250         File appPath = new File(MuleContainerBootstrapUtils.getMuleAppsDir(), getAppName());
251         appProperties.put(MuleProperties.APP_HOME_DIRECTORY_PROPERTY, appPath.getAbsolutePath());
252 
253         appProperties.put(MuleProperties.APP_NAME_PROPERTY, getAppName());
254 
255         return new SimpleConfigurationBuilder(appProperties);
256     }
257 
258     protected void addAnnotationsConfigBuilderIfPresent(List<ConfigurationBuilder> builders) throws Exception
259     {
260         // If the annotations module is on the classpath, add the annotations config builder to
261         // the list. This will enable annotations config for this instance.
262         if (ClassUtils.isClassOnPath(MuleServer.CLASSNAME_ANNOTATIONS_CONFIG_BUILDER, getClass()))
263         {
264             Object configBuilder = ClassUtils.instanciateClass(
265                 MuleServer.CLASSNAME_ANNOTATIONS_CONFIG_BUILDER, ClassUtils.NO_ARGS, getClass());
266             builders.add((ConfigurationBuilder) configBuilder);
267         }
268     }
269 
270     protected void addIBeansConfigurationBuilderIfPackagesConfiguredForScanning(List<ConfigurationBuilder> builders)
271         throws Exception
272     {
273         String packagesToScan = descriptor.getPackagesToScan();
274         if (StringUtils.isNotEmpty(packagesToScan))
275         {
276             String[] paths = packagesToScan.split(",");
277             Object configBuilder = ClassUtils.instanciateClass(
278                 MuleServer.CLASSNAME_IBEANS_CONFIG_BUILDER, new Object[] { paths }, getClass());
279             builders.add((ConfigurationBuilder) configBuilder);
280         }
281     }
282 
283     @Override
284     public MuleContext getMuleContext()
285     {
286         return muleContext;
287     }
288 
289     @Override
290     public ClassLoader getDeploymentClassLoader()
291     {
292         return this.deploymentClassLoader;
293     }
294 
295     @Override
296     public void dispose()
297     {
298         // moved wrapper logic into the actual implementation, as redeploy() invokes it directly, bypassing
299         // classloader cleanup
300         try
301         {
302             ClassLoader appCl = getDeploymentClassLoader();
303             // if not initialized yet, it can be null
304             if (appCl != null)
305             {
306                 Thread.currentThread().setContextClassLoader(appCl);
307             }
308 
309             doDispose();
310 
311             if (appCl != null)
312             {
313                 // close classloader to release jar connections in lieu of Java 7's ClassLoader.close()
314                 if (appCl instanceof GoodCitizenClassLoader)
315                 {
316                     GoodCitizenClassLoader classLoader = (GoodCitizenClassLoader) appCl;
317                     classLoader.close();
318                 }
319             }
320         }
321         finally
322         {
323             // kill any refs to the old classloader to avoid leaks
324             Thread.currentThread().setContextClassLoader(null);
325         }
326     }
327 
328     @Override
329     public void redeploy()
330     {
331         if (logger.isInfoEnabled())
332         {
333             logger.info(miniSplash(String.format("Redeploying app '%s'", descriptor.getAppName())));
334         }
335 
336         String appName = getAppName();
337 
338         this.deploymentService.fireOnUndeploymentStart(appName);
339         try
340         {
341             dispose();
342 
343             this.deploymentService.fireOnUndeploymentSuccess(appName);
344         }
345         catch (RuntimeException e)
346         {
347             this.deploymentService.fireOnUndeploymentFailure(appName, e);
348 
349             throw e;
350         }
351 
352         install();
353 
354         // update thread with the fresh new classloader just created during the install phase
355         final ClassLoader cl = getDeploymentClassLoader();
356         Thread.currentThread().setContextClassLoader(cl);
357 
358         this.deploymentService.fireOnDeploymentStart(appName);
359         try
360         {
361             init();
362             start();
363 
364             this.deploymentService.fireOnDeploymentSuccess(appName);
365         }
366         catch(Throwable cause)
367         {
368             this.deploymentService.fireOnDeploymentFailure(appName, cause);
369         }
370 
371         // release the ref
372         Thread.currentThread().setContextClassLoader(null);
373     }
374 
375     @Override
376     public void stop()
377     {
378         if (this.muleContext == null)
379         {
380             // app never started, maybe due to a previous error
381             return;
382         }
383         if (logger.isInfoEnabled())
384         {
385             logger.info(miniSplash(String.format("Stopping app '%s'", descriptor.getAppName())));
386         }
387         try
388         {
389             this.muleContext.stop();
390         }
391         catch (MuleException e)
392         {
393             // TODO add app name to the exception field
394             throw new DeploymentStopException(MessageFactory.createStaticMessage(descriptor.getAppName()), e);
395         }
396     }
397 
398     @Override
399     public String toString()
400     {
401         return String.format("%s[%s]@%s", getClass().getName(),
402                              descriptor.getAppName(),
403                              Integer.toHexString(System.identityHashCode(this)));
404     }
405 
406     protected void doDispose()
407     {
408         if (muleContext == null)
409         {
410             if (logger.isInfoEnabled())
411             {
412                 logger.info(String.format("App '%s' never started, nothing to dispose of", descriptor.getAppName()));
413             }
414             return;
415         }
416 
417         if (muleContext.isStarted() && !muleContext.isDisposed())
418         {
419             try
420             {
421                 stop();
422             }
423             catch (DeploymentStopException e)
424             {
425                 // catch the stop errors and just log, we're disposing of an app anyway
426                 logger.error(e);
427             }
428         }
429         if (logger.isInfoEnabled())
430         {
431             logger.info(miniSplash(String.format("Disposing app '%s'", descriptor.getAppName())));
432         }
433 
434         muleContext.dispose();
435         muleContext = null;
436     }
437 
438     protected void createDeploymentClassLoader()
439     {
440         final String domain = descriptor.getDomain();
441         ClassLoader parent;
442 
443         if (StringUtils.isBlank(domain) || DefaultMuleSharedDomainClassLoader.DEFAULT_DOMAIN_NAME.equals(domain))
444         {
445             parent = new DefaultMuleSharedDomainClassLoader(getClass().getClassLoader());
446         }
447         else
448         {
449             // TODO handle non-existing domains with an exception
450             parent = new MuleSharedDomainClassLoader(domain, getClass().getClassLoader());
451         }
452 
453         final Set<PluginDescriptor> plugins = descriptor.getPlugins();
454         if (!plugins.isEmpty())
455         {
456             MulePluginsClassLoader cl = new MulePluginsClassLoader(parent, plugins);
457             // re-assign parent ref if any plugins deployed, will be used by the MuleAppCL
458             parent = cl;
459         }
460 
461         final MuleApplicationClassLoader appCl = new MuleApplicationClassLoader(descriptor.getAppName(),
462                                                                                 parent,
463                                                                                 descriptor.getLoaderOverride());
464         this.deploymentClassLoader = appCl;
465     }
466 
467     protected void createRedeployMonitor() throws NotificationException
468     {
469         if (logger.isInfoEnabled())
470         {
471             logger.info("Monitoring for hot-deployment: " + new File(absoluteResourcePaths [0]));
472         }
473 
474         final AbstractFileWatcher watcher = new ConfigFileWatcher(new File(absoluteResourcePaths [0]));
475 
476         // register a config monitor only after context has started, as it may take some time
477         muleContext.registerListener(new MuleContextNotificationListener<MuleContextNotification>()
478         {
479             @Override
480             public void onNotification(MuleContextNotification notification)
481             {
482                 final int action = notification.getAction();
483                 switch (action)
484                 {
485                     case MuleContextNotification.CONTEXT_STARTED:
486                         scheduleConfigMonitor(watcher);
487                         break;
488                     case MuleContextNotification.CONTEXT_STOPPING:
489                         if (watchTimer != null)
490                         {
491                             // edge case when app startup was interrupted and we haven't started monitoring it yet
492                             watchTimer.shutdownNow();
493                         }
494                         muleContext.unregisterListener(this);
495                         break;
496                 }
497             }
498         });
499     }
500 
501     protected void scheduleConfigMonitor(AbstractFileWatcher watcher)
502     {
503         final int reloadIntervalMs = DEFAULT_RELOAD_CHECK_INTERVAL_MS;
504         watchTimer = Executors.newSingleThreadScheduledExecutor(new ConfigChangeMonitorThreadFactory(descriptor.getAppName()));
505 
506         watchTimer.scheduleWithFixedDelay(watcher, reloadIntervalMs, reloadIntervalMs, TimeUnit.MILLISECONDS);
507 
508         if (logger.isInfoEnabled())
509         {
510             logger.info("Reload interval: " + reloadIntervalMs);
511         }
512     }
513 
514     /**
515      * Resolve a resource relative to an application root.
516      * @param path the relative path to resolve
517      * @return absolute path, may not actually exist (check with File.exists())
518      */
519     protected File toAbsoluteFile(String path)
520     {
521         final String muleHome = System.getProperty(MuleProperties.MULE_HOME_DIRECTORY_PROPERTY);
522         String configPath = String.format("%s/apps/%s/%s", muleHome, getAppName(), path);
523         return new File(configPath);
524     }
525 
526     protected class ConfigFileWatcher extends AbstractFileWatcher
527     {
528         public ConfigFileWatcher(File watchedResource)
529         {
530             super(watchedResource);
531         }
532 
533         @Override
534         protected synchronized void onChange(File file)
535         {
536             if (logger.isInfoEnabled())
537             {
538                 logger.info("================== Reloading " + file);
539             }
540 
541             // grab the proper classloader for our context
542             final ClassLoader cl = getDeploymentClassLoader();
543             Thread.currentThread().setContextClassLoader(cl);
544             redeploy();
545         }
546     }
547 }