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