View Javadoc

1   // ========================================================================
2   // Copyright (c) 2009-2010 Mortbay, Inc.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at 
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses. 
12  // Contributors:
13  //    Greg Wilkins - initial API and implementation
14  // ========================================================================
15  package org.eclipse.jetty.osgi.boot;
16  
17  import java.io.File;
18  import java.io.FilenameFilter;
19  import java.io.IOException;
20  import java.util.HashSet;
21  import java.util.Iterator;
22  import java.util.Map.Entry;
23  import java.util.Set;
24  
25  import org.eclipse.jetty.deploy.App;
26  import org.eclipse.jetty.deploy.AppProvider;
27  import org.eclipse.jetty.deploy.DeploymentManager;
28  import org.eclipse.jetty.deploy.providers.ContextProvider;
29  import org.eclipse.jetty.deploy.providers.ScanningAppProvider;
30  import org.eclipse.jetty.osgi.boot.utils.internal.PackageAdminServiceTracker;
31  import org.eclipse.jetty.server.handler.ContextHandler;
32  import org.eclipse.jetty.util.Scanner;
33  import org.eclipse.jetty.util.log.Log;
34  import org.eclipse.jetty.util.log.Logger;
35  import org.eclipse.jetty.util.resource.Resource;
36  import org.eclipse.jetty.webapp.WebAppContext;
37  import org.osgi.framework.Bundle;
38  import org.osgi.framework.BundleContext;
39  import org.osgi.framework.BundleException;
40  import org.osgi.framework.Constants;
41  
42  /**
43   * AppProvider for OSGi. Supports the configuration of ContextHandlers and
44   * WebApps. Extends the AbstractAppProvider to support the scanning of context
45   * files located outside of the bundles.
46   * <p>
47   * This provider must not be called outside of jetty.boot: it should always be
48   * called via the OSGi service listener.
49   * </p>
50   * <p>
51   * This provider supports the same set of parameters than the WebAppProvider as
52   * it supports the deployment of WebAppContexts. Except for the scanning of the
53   * webapps directory.
54   * </p>
55   * <p>
56   * When the parameter autoInstallOSGiBundles is set to true, OSGi bundles that
57   * are located in the monitored directory are installed and started after the
58   * framework as finished auto-starting all the other bundles.
59   * Warning: only use this for development.
60   * </p>
61   */
62  public class OSGiAppProvider extends ScanningAppProvider implements AppProvider
63  {
64      private static final Logger LOG = Log.getLogger(OSGiAppProvider.class);
65  
66  
67      private boolean _extractWars = true;
68      private boolean _parentLoaderPriority = false;
69      private String _defaultsDescriptor;
70      private String _tldBundles;
71      private String[] _configurationClasses;
72      
73      private boolean _autoInstallOSGiBundles = true;
74      
75      //Keep track of the bundles that were installed and that are waiting for the
76      //framework to complete its initialization.
77      Set<Bundle> _pendingBundlesToStart = null;
78          
79      /**
80       * When a context file corresponds to a deployed bundle and is changed we
81       * reload the corresponding bundle.
82       */
83      private static class Filter implements FilenameFilter
84      {
85          OSGiAppProvider _enclosedInstance;
86          
87          public boolean accept(File dir, String name)
88          {
89              File file = new File(dir,name);
90              if (fileMightBeAnOSGiBundle(file))
91              {
92                  return true;
93              }
94              if (!file.isDirectory())
95              {
96                  String contextName = getDeployedAppName(name);
97                  if (contextName != null)
98                  {
99                      App app = _enclosedInstance.getDeployedApps().get(contextName);
100                     return app != null;
101                 }
102             }
103             return false;
104         }
105     }
106 
107     /**
108      * @param contextFileName
109      *            for example myContext.xml
110      * @return The context, for example: myContext; null if this was not a
111      *         suitable contextFileName.
112      */
113     private static String getDeployedAppName(String contextFileName)
114     {
115         String lowername = contextFileName.toLowerCase();
116         if (lowername.endsWith(".xml"))
117         {
118             String contextName = contextFileName.substring(0,lowername.length() - ".xml".length());
119             return contextName;
120         }
121         return null;
122     }
123 
124     /**
125      * Reading the display name of a webapp is really not sufficient for indexing the various
126      * deployed ContextHandlers.
127      * 
128      * @param context
129      * @return
130      */
131     private String getContextHandlerAppName(ContextHandler context) {
132         String appName = context.getDisplayName();
133         if (appName == null || appName.length() == 0  || getDeployedApps().containsKey(appName)) {
134         	if (context instanceof WebAppContext)
135         	{
136         		appName = ((WebAppContext)context).getContextPath();
137         		if (getDeployedApps().containsKey(appName)) {
138             		appName = "noDisplayName"+context.getClass().getSimpleName()+context.hashCode();
139             	}
140         	} else {
141         		appName = "noDisplayName"+context.getClass().getSimpleName()+context.hashCode();
142         	}
143         }
144         return appName;
145     }
146     
147     /**
148      * Default OSGiAppProvider consutructed when none are defined in the
149      * jetty.xml configuration.
150      */
151     public OSGiAppProvider()
152     {
153         super(new Filter());
154         ((Filter)super._filenameFilter)._enclosedInstance = this;
155     }
156 
157     /**
158      * Default OSGiAppProvider consutructed when none are defined in the
159      * jetty.xml configuration.
160      * 
161      * @param contextsDir
162      */
163     public OSGiAppProvider(File contextsDir) throws IOException
164     {
165         this();
166         setMonitoredDirResource(Resource.newResource(contextsDir.toURI()));
167     }
168     
169     /**
170      * Returns the ContextHandler that was created by WebappRegistractionHelper
171      * 
172      * @see AppProvider
173      */
174     public ContextHandler createContextHandler(App app) throws Exception
175     {
176         // return pre-created Context
177         ContextHandler wah = app.getContextHandler();
178         if (wah == null)
179         {
180             // for some reason it was not defined when the App was constructed.
181             // we don't support this situation at this point.
182             // once the WebAppRegistrationHelper is refactored, the code
183             // that creates the ContextHandler will actually be here.
184             throw new IllegalStateException("The App must be passed the " + "instance of the ContextHandler when it is construsted");
185         }
186         if (_configurationClasses != null && wah instanceof WebAppContext) 
187         {
188             ((WebAppContext)wah).setConfigurationClasses(_configurationClasses);
189         }
190         return app.getContextHandler();
191     }
192 
193     /**
194      * @see AppProvider
195      */
196     @Override
197     public void setDeploymentManager(DeploymentManager deploymentManager)
198     {
199         // _manager=deploymentManager;
200         super.setDeploymentManager(deploymentManager);
201     }
202 
203     private static String getOriginId(Bundle contributor, String pathInBundle)
204     {
205     	return contributor.getSymbolicName() + "-" + contributor.getVersion().toString() +
206     		(pathInBundle.startsWith("/") ? pathInBundle : "/" + pathInBundle);
207     }
208     
209     /**
210      * @param context
211      * @throws Exception
212      */
213     public void addContext(Bundle contributor, String pathInBundle, ContextHandler context) throws Exception
214     {
215     	addContext(getOriginId(contributor, pathInBundle), context);
216     }
217     /**
218      * @param context
219      * @throws Exception
220      */
221     public void addContext(String originId, ContextHandler context) throws Exception
222     {
223         // TODO apply configuration specific to this provider
224     	if (context instanceof WebAppContext)
225     	{
226            ((WebAppContext)context).setExtractWAR(isExtract());
227     	}
228 
229         // wrap context as an App
230         App app = new App(getDeploymentManager(),this,originId,context);
231         String appName = getContextHandlerAppName(context);
232         getDeployedApps().put(appName,app);
233         getDeploymentManager().addApp(app);
234     }
235     
236     
237 
238     /**
239      * Called by the scanner of the context files directory. If we find the
240      * corresponding deployed App we reload it by returning the App. Otherwise
241      * we return null and nothing happens: presumably the corresponding OSGi
242      * webapp is not ready yet.
243      * 
244      * @return the corresponding already deployed App so that it will be
245      *         reloaded. Otherwise returns null.
246      */
247     @Override
248     protected App createApp(String filename)
249     {
250         // find the corresponding bundle and ContextHandler or WebAppContext
251         // and reload the corresponding App.
252         // see the 2 pass of the refactoring of the WebAppRegistrationHelper.
253         String name = getDeployedAppName(filename);
254         if (name != null)
255         {
256             return getDeployedApps().get(name);
257         }
258         return null;
259     }
260 
261     public void removeContext(ContextHandler context) throws Exception
262     {
263     	String appName = getContextHandlerAppName(context);
264         App app = getDeployedApps().remove(context.getDisplayName());
265         if (app == null) {
266         	//try harder to undeploy this context handler.
267         	//see bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=330098
268         	appName = null;
269         	for (Entry<String,App> deployedApp : getDeployedApps().entrySet()) {
270         		if (deployedApp.getValue().getContextHandler() == context) {
271         			app = deployedApp.getValue();
272         			appName = deployedApp.getKey();
273         			break;
274         		}
275         	}
276         	if (appName != null) {
277         		getDeployedApps().remove(appName);
278         	}
279         }
280         if (app != null)
281         {
282             getDeploymentManager().removeApp(app);
283         }
284     }
285 
286     // //copied from WebAppProvider as the parameters are identical.
287     // //only removed the parameer related to extractWars.
288     /* ------------------------------------------------------------ */
289     /**
290      * Get the parentLoaderPriority.
291      * 
292      * @return the parentLoaderPriority
293      */
294     public boolean isParentLoaderPriority()
295     {
296         return _parentLoaderPriority;
297     }
298 
299     /* ------------------------------------------------------------ */
300     /**
301      * Set the parentLoaderPriority.
302      * 
303      * @param parentLoaderPriority
304      *            the parentLoaderPriority to set
305      */
306     public void setParentLoaderPriority(boolean parentLoaderPriority)
307     {
308         _parentLoaderPriority = parentLoaderPriority;
309     }
310 
311     /* ------------------------------------------------------------ */
312     /**
313      * Get the defaultsDescriptor.
314      * 
315      * @return the defaultsDescriptor
316      */
317     public String getDefaultsDescriptor()
318     {
319         return _defaultsDescriptor;
320     }
321 
322     /* ------------------------------------------------------------ */
323     /**
324      * Set the defaultsDescriptor.
325      * 
326      * @param defaultsDescriptor
327      *            the defaultsDescriptor to set
328      */
329     public void setDefaultsDescriptor(String defaultsDescriptor)
330     {
331         _defaultsDescriptor = defaultsDescriptor;
332     }
333 
334     /**
335      * The context xml directory. In fact it is the directory watched by the
336      * scanner.
337      */
338     public File getContextXmlDirAsFile()
339     {
340         try
341         {
342             Resource monitoredDir = getMonitoredDirResource();
343             if (monitoredDir == null)
344                 return null;
345             return monitoredDir.getFile();
346         }
347         catch (IOException e)
348         {
349             e.printStackTrace();
350             return null;
351         }
352     }
353 
354     /* ------------------------------------------------------------ */
355     /**
356      * The context xml directory. In fact it is the directory watched by the
357      * scanner.
358      */
359     public String getContextXmlDir()
360     {
361         try
362         {
363             Resource monitoredDir = getMonitoredDirResource();
364             if (monitoredDir == null)
365                 return null;
366             return monitoredDir.getFile().toURI().toString();
367         }
368         catch (IOException e)
369         {
370             e.printStackTrace();
371             return null;
372         }
373     }
374     
375     public boolean isExtract()
376     {
377         return _extractWars;
378     }
379 
380     public void setExtract(boolean extract)
381     {
382         _extractWars=extract;
383     }
384 
385     /**
386      * @return true when this app provider locates osgi bundles and features in
387      * its monitored directory and installs them. By default true if there is a folder to monitor.
388      */
389     public boolean isAutoInstallOSGiBundles()
390     {
391     	return _autoInstallOSGiBundles;
392     }
393 
394     /**
395      * &lt;autoInstallOSGiBundles&gt;true&lt;/autoInstallOSGiBundles&gt;
396      * @param installingOSGiBundles
397      */
398     public void setAutoInstallOSGiBundles(boolean installingOSGiBundles)
399     {
400         _autoInstallOSGiBundles=installingOSGiBundles;
401     }
402 
403 
404     /* ------------------------------------------------------------ */
405     /**
406      * Set the directory in which to look for context XML files.
407      * <p>
408      * If a webapp call "foo/" or "foo.war" is discovered in the monitored
409      * directory, then the ContextXmlDir is examined to see if a foo.xml file
410      * exists. If it does, then this deployer will not deploy the webapp and the
411      * ContextProvider should be used to act on the foo.xml file.
412      * </p>
413      * <p>
414      * Also if this directory contains some osgi bundles, it will install them.
415      * </p>
416      * 
417      * @see ContextProvider
418      * @param contextsDir
419      */
420     public void setContextXmlDir(String contextsDir)
421     {
422         setMonitoredDirName(contextsDir);
423     }
424     
425     /**
426      * @param tldBundles Comma separated list of bundles that contain tld jars
427      * that should be setup on the jetty instances created here.
428      */
429     public void setTldBundles(String tldBundles)
430     {
431     	_tldBundles = tldBundles;
432     }
433     
434     /**
435      * @return The list of bundles that contain tld jars that should be setup
436      * on the jetty instances created here.
437      */
438     public String getTldBundles()
439     {
440     	return _tldBundles;
441     }
442     
443     /**
444      * @param configurations The configuration class names.
445      */
446     public void setConfigurationClasses(String[] configurations)
447     {
448         _configurationClasses = configurations==null?null:(String[])configurations.clone();
449     }  
450     
451     /* ------------------------------------------------------------ */
452     /**
453      * 
454      */
455     public String[] getConfigurationClasses()
456     {
457         return _configurationClasses;
458     }
459 
460     /**
461      * Overridden to install the OSGi bundles found in the monitored folder.
462      */
463     @Override
464     protected void doStart() throws Exception
465     {
466         if (isAutoInstallOSGiBundles())
467         {
468         	if (getMonitoredDirResource()  == null)
469         	{
470         		setAutoInstallOSGiBundles(false);
471         		LOG.info("Disable autoInstallOSGiBundles as there is not contexts folder to monitor.");
472         	}
473 	    	else
474         	{
475 	    		File scandir = null;
476 	    		try
477 	    		{
478 	                scandir = getMonitoredDirResource().getFile();
479 	                if (!scandir.exists() || !scandir.isDirectory())
480 	                {
481 	                	setAutoInstallOSGiBundles(false);
482 	            		LOG.warn("Disable autoInstallOSGiBundles as the contexts folder '" + scandir.getAbsolutePath() + " does not exist.");
483 	            		scandir = null;
484 	                }
485 	    		}
486 	    		catch (IOException ioe)
487 	    		{
488                 	setAutoInstallOSGiBundles(false);
489             		LOG.warn("Disable autoInstallOSGiBundles as the contexts folder '" + getMonitoredDirResource().getURI() + " does not exist.");
490             		scandir = null;
491 	    		}
492 	    		if (scandir != null)
493 	    		{
494 		            for (File file : scandir.listFiles())
495 		            {
496 		                if (fileMightBeAnOSGiBundle(file))
497 		                {
498 		                    installBundle(file, false);
499 		                }
500 		            }
501 	    		}
502         	}
503         }
504         super.doStart();
505         if (isAutoInstallOSGiBundles())
506         {
507             Scanner.ScanCycleListener scanCycleListner = new AutoStartWhenFrameworkHasCompleted(this);
508             super.addScannerListener(scanCycleListner);
509         }
510     }
511     
512     /**
513      * When the file is a jar or a folder, we look if it looks like an OSGi bundle.
514      * In that case we install it and start it.
515      * <p>
516      * Really a simple trick to get going quickly with development.
517      * </p>
518      */
519     @Override
520     protected void fileAdded(String filename) throws Exception
521     {
522         File file = new File(filename);
523         if (isAutoInstallOSGiBundles() && file.exists() && fileMightBeAnOSGiBundle(file))
524         {
525             installBundle(file, true);
526         }
527         else
528         {
529             super.fileAdded(filename);
530         }
531     }
532     
533     /**
534      * @param file
535      * @return
536      */
537     private static boolean fileMightBeAnOSGiBundle(File file)
538     {
539         if (file.isDirectory())
540         {
541             if (new File(file,"META-INF/MANIFEST.MF").exists())
542             {
543                 return true;
544             }
545         }
546         else if (file.getName().endsWith(".jar"))
547         {
548             return true;
549         }
550         return false;
551     }
552 
553     @Override
554     protected void fileChanged(String filename) throws Exception
555     {
556         File file = new File(filename);
557         if (isAutoInstallOSGiBundles() && fileMightBeAnOSGiBundle(file))
558         {
559             updateBundle(file);
560         }
561         else
562         {
563             super.fileChanged(filename);
564         }
565     }
566 
567     @Override
568     protected void fileRemoved(String filename) throws Exception
569     {
570         File file = new File(filename);
571         if (isAutoInstallOSGiBundles() && fileMightBeAnOSGiBundle(file))
572         {
573             uninstallBundle(file);
574         }
575         else
576         {
577             super.fileRemoved(filename);
578         }
579     }
580     
581     /**
582      * Returns a bundle according to its location.
583      * In the version 1.6 of org.osgi.framework, BundleContext.getBundle(String) is what we want.
584      * However to support older versions of OSGi. We use our own local refrence mechanism.
585      * @param location
586      * @return
587      */
588     protected Bundle getBundle(BundleContext bc, String location)
589     {
590     	//not available in older versions of OSGi:
591     	//return bc.getBundle(location);
592     	for (Bundle b : bc.getBundles())
593     	{
594     		if (b.getLocation().equals(location))
595     		{
596     			return b;
597     		}
598     	}
599     	return null;
600     }
601 
602     protected synchronized Bundle installBundle(File file, boolean start)
603     {
604     	
605         try
606         {
607             BundleContext bc = JettyBootstrapActivator.getBundleContext();
608             String location = file.toURI().toString();
609             Bundle b = getBundle(bc, location);
610             if (b == null) 
611             {
612                 b = bc.installBundle(location);
613             }
614             if (b == null)
615             {
616             	//not sure we will ever be here,
617             	//most likely a BundleException was thrown
618             	LOG.warn("The file " + location + " is not an OSGi bundle.");
619             	return null;
620             }
621             if (start && b.getHeaders().get(Constants.FRAGMENT_HOST) == null)
622             {//not a fragment, try to start it. if the framework has finished auto-starting.
623             	if (!PackageAdminServiceTracker.INSTANCE.frameworkHasCompletedAutostarts())
624             	{
625             		if (_pendingBundlesToStart == null)
626             		{
627             			_pendingBundlesToStart = new HashSet<Bundle>();
628             		}
629             		_pendingBundlesToStart.add(b);
630             		return null;
631             	}
632             	else
633             	{
634             		b.start();
635             	}
636             }
637             return b;
638         }
639         catch (BundleException e)
640         {
641             LOG.warn("Unable to " + (start? "start":"install") + " the bundle " + file.getAbsolutePath(), e);
642         }
643         return null;
644     }
645     
646     protected void uninstallBundle(File file)
647     {
648         try
649         {
650             Bundle b = getBundle(JettyBootstrapActivator.getBundleContext(), file.toURI().toString());
651             b.stop();
652             b.uninstall();
653         }
654         catch (BundleException e)
655         {
656             LOG.warn("Unable to uninstall the bundle " + file.getAbsolutePath(), e);
657         }
658     }
659     
660     protected void updateBundle(File file)
661     {
662         try
663         {
664             Bundle b = getBundle(JettyBootstrapActivator.getBundleContext(), file.toURI().toString());
665             if (b == null)
666             {
667                 installBundle(file, true);
668             }
669             else if (b.getState() == Bundle.ACTIVE)
670             {
671                 b.update();
672             }
673             else
674             {
675                 b.start();
676             }
677         }
678         catch (BundleException e)
679         {
680             LOG.warn("Unable to update the bundle " + file.getAbsolutePath(), e);
681         }
682     }
683     
684 
685 }
686 /**
687  * At the end of each scan, if there are some bundles to be started,
688  * look if the framework has completed its autostart. In that case start those bundles.
689  */
690 class AutoStartWhenFrameworkHasCompleted implements Scanner.ScanCycleListener
691 {
692         private static final Logger LOG = Log.getLogger(AutoStartWhenFrameworkHasCompleted.class);
693     
694 	private final OSGiAppProvider _appProvider;
695 	
696 	AutoStartWhenFrameworkHasCompleted(OSGiAppProvider appProvider)
697 	{
698 		_appProvider = appProvider;
699 	}
700 	
701 	public void scanStarted(int cycle) throws Exception
702 	{
703 	}
704 	
705 	public void scanEnded(int cycle) throws Exception
706 	{
707 		if (_appProvider._pendingBundlesToStart != null && PackageAdminServiceTracker.INSTANCE.frameworkHasCompletedAutostarts())
708 		{
709 			Iterator<Bundle> it = _appProvider._pendingBundlesToStart.iterator();
710 			while (it.hasNext())
711 			{
712 				Bundle b = it.next();
713 				if (b.getHeaders().get(Constants.FRAGMENT_HOST) != null)
714 				{
715 					continue;
716 				}
717 				try
718 				{
719 					b.start();
720 				}
721 		        catch (BundleException e)
722 		        {
723 		            LOG.warn("Unable to start the bundle " + b.getLocation(), e);
724 		        }
725 
726 			}
727 			_appProvider._pendingBundlesToStart = null;
728 		}
729 	}
730 
731 }
732