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