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