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