View Javadoc
1   package com.atlassian.plugin.osgi.factory;
2   
3   import com.atlassian.plugin.IllegalPluginStateException;
4   import com.atlassian.plugin.Plugin;
5   import com.atlassian.plugin.PluginArtifact;
6   import com.atlassian.plugin.PluginDependencies;
7   import com.atlassian.plugin.PluginException;
8   import com.atlassian.plugin.PluginState;
9   import com.atlassian.plugin.impl.AbstractPlugin;
10  import com.atlassian.plugin.module.ContainerAccessor;
11  import com.atlassian.plugin.module.ContainerManagedPlugin;
12  import com.atlassian.plugin.osgi.container.OsgiContainerException;
13  import com.atlassian.plugin.osgi.container.OsgiContainerManager;
14  import com.atlassian.plugin.osgi.util.BundleClassLoaderAccessor;
15  import com.atlassian.plugin.osgi.util.OsgiPluginUtil;
16  import com.atlassian.plugin.util.resource.AlternativeDirectoryResourceLoader;
17  import org.osgi.framework.Bundle;
18  import org.osgi.framework.BundleEvent;
19  import org.osgi.framework.BundleException;
20  import org.osgi.framework.Constants;
21  import org.osgi.framework.ServiceReference;
22  import org.osgi.framework.SynchronousBundleListener;
23  import org.osgi.service.packageadmin.PackageAdmin;
24  import org.osgi.util.tracker.ServiceTracker;
25  import org.osgi.util.tracker.ServiceTrackerCustomizer;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  
29  import javax.annotation.Nonnull;
30  import javax.annotation.Nullable;
31  import java.io.File;
32  import java.io.InputStream;
33  import java.net.URL;
34  import java.util.Date;
35  import java.util.jar.Manifest;
36  
37  import static com.atlassian.plugin.osgi.util.OsgiHeaderUtil.extractOsgiPluginInformation;
38  import static com.atlassian.plugin.osgi.util.OsgiHeaderUtil.getAttributeWithoutValidation;
39  import static com.atlassian.plugin.osgi.util.OsgiHeaderUtil.getManifest;
40  import static com.google.common.base.Preconditions.checkNotNull;
41  
42  /**
43   * Plugin that wraps an OSGi bundle
44   * <p>
45   * That kind of plugins might have or do not have plugin descriptor and must manage internal IoC by themselves
46   * if it is present. Note that some modules require access to plugin IoC to work properly so it is plugin
47   * responsibility to provide a such access and manage internal container lifecycle.
48   * <p>
49   * To export any container to PluginsFramework bundle should export ContainerAccessor interface implementation
50   * as a service before move to ACTIVE state. Behavior is not defined if plugin provides more then one instances of
51   * ContainerAccessor
52   *
53   * Plugin of that type has lifecycle duplicates underlying OSGi Bundle natural states
54   */
55  public class OsgiBundlePlugin extends AbstractPlugin implements OsgiBackedPlugin, ContainerManagedPlugin, SynchronousBundleListener {
56      private static final Logger log = LoggerFactory.getLogger(OsgiBundlePlugin.class);
57  
58      /*
59       * Notes on concurrency:
60       * All methods with *Internal suffix are called from corresponding wrappers from the base class,
61       * i.e installInternal from install. Wrapper call takes care about plugin state update which is effectively
62       * volatile variable write (AtomicReference.set). That write is a guarantee of all internal state changes safe
63       * publication. 
64       * 
65       * However methods are *not* thread safe, as there is no protection against race execution. In some cases
66       * that won't lead to any problems except not optimal resource management but special care should be taken in
67       * each particular scenario to guarantee data structures valid states
68       * 
69       * Make sure that if any of *Internal method returns PENDING state it takes care to make changes safely 
70       * published in terms of concurrency
71       */
72  
73      private final Date dateLoaded;
74  
75      /**
76       * The OSGi container manager, gateway to underlaying OSGi container
77       */
78      private OsgiContainerManager osgiContainerManager;
79  
80      /**
81       * The OSGi bundle, which will be null until installInternal is called.
82       */
83      private volatile Bundle bundle;
84  
85      /**
86       * The ClassLoader for the OSGi bundle, which will be null until installInternal is called.
87       */
88      private ClassLoader bundleClassLoader;
89  
90      /*
91       * Service exported by bundle to provide access to internal IoC.
92       * Field gets initialized on the first access 
93       */
94      private
95      @Nullable
96      ServiceTracker<ContainerAccessor, ContainerAccessor> containerAccessorTracker;
97  
98      /*
99       * Service to be used to calculate bundle wiring and extract plugin dependencies in terms of
100      * Atlassian Plugins Framework
101      */
102     private
103     @Nullable
104     ServiceTracker<PackageAdmin, PackageAdmin> pkgAdminService;
105 
106     private OsgiBundlePlugin(final String pluginKey, final PluginArtifact pluginArtifact) {
107         super(checkNotNull(pluginArtifact));
108         this.dateLoaded = new Date();
109         setPluginsVersion(2);
110         setKey(pluginKey);
111         setSystemPlugin(false);
112     }
113 
114     @Override
115     public Bundle getBundle() throws IllegalPluginStateException {
116         if (bundle == null) {
117             throw new IllegalPluginStateException("This operation must occur while the plugin '" + getKey() + "' is installed");
118         }
119 
120         return bundle;
121     }
122 
123     /**
124      * Create a plugin wrapper which installs the bundle when the plugin is installed.
125      *
126      * @param osgiContainerManager the container to install into when the plugin is installed.
127      * @param pluginKey            The plugin key.
128      * @param pluginArtifact       The The plugin artifact to install.
129      */
130     public OsgiBundlePlugin(final OsgiContainerManager osgiContainerManager,
131                             final String pluginKey,
132                             final PluginArtifact pluginArtifact) {
133         this(pluginKey, pluginArtifact);
134         this.osgiContainerManager = checkNotNull(osgiContainerManager);
135         // Leave bundle and bundleClassLoader null until we are installed.
136 
137         final Manifest manifest = getManifest(pluginArtifact);
138         if (null != manifest) {
139             setName(getAttributeWithoutValidation(manifest, Constants.BUNDLE_NAME));
140             // The next false is because at the OSGi level, Bundle-Version is not required.
141             setPluginInformation(extractOsgiPluginInformation(manifest, false));
142         }
143         // else this will get flagged as a bad jar, because it's not a bundle, later, and we can let this through
144     }
145 
146     @Override
147     public Date getDateLoaded() {
148         return dateLoaded;
149     }
150 
151     @Override
152     public Date getDateInstalled() {
153         long date = getPluginArtifact().toFile().lastModified();
154         if (date == 0) {
155             date = getDateLoaded().getTime();
156         }
157         return new Date(date);
158     }
159 
160     @Override
161     public boolean isUninstallable() {
162         return true;
163     }
164 
165     @Override
166     public boolean isDeleteable() {
167         return true;
168     }
169 
170     @Override
171     public boolean isDynamicallyLoaded() {
172         return true;
173     }
174 
175     @Override
176     public <T> Class<T> loadClass(final String clazz, final Class<?> callingClass) throws ClassNotFoundException {
177         return BundleClassLoaderAccessor.loadClass(getBundleOrFail(), clazz);
178     }
179 
180     @Override
181     public URL getResource(final String name) {
182         return getBundleClassLoaderOrFail().getResource(name);
183     }
184 
185     @Override
186     public InputStream getResourceAsStream(final String name) {
187         return getBundleClassLoaderOrFail().getResourceAsStream(name);
188     }
189 
190     @Override
191     public void resolve() {
192         // Force resolve underlaying bundle. That method gets called by PluginEnabler
193         // to perform resolving of plugins set in parallel before actual dependency resolution
194         if (pkgAdminService == null) {
195             // Should never happens actually
196             return;
197         }
198         PackageAdmin packageAdmin = pkgAdminService.getService();
199         packageAdmin.resolveBundles(new Bundle[]{bundle});
200     }
201 
202     /**
203      * @see Plugin#getDependencies()
204      */
205     @Nonnull
206     @Override
207     public PluginDependencies getDependencies() {
208         // OSGiBundlePlugin might depends on other bundles in the OSGI instance, some of 
209         // that bundles might be plugins, so it is make sense to implement method to allow
210         // PluginEnabler process dependencies correctly
211         if (this.getPluginState() == PluginState.UNINSTALLED) {
212             throw new IllegalPluginStateException("This operation requires the plugin '" + getKey() + "' to be installed");
213         }
214         return OsgiPluginUtil.getDependencies(bundle);
215     }
216 
217     @Override
218     protected void installInternal() throws OsgiContainerException, IllegalPluginStateException {
219         super.installInternal();
220         if (null != osgiContainerManager) {
221             // During all time when bundle exists Plugin must reflects state of OSGi bundle, listener below
222             // takes care of it. Note that listener registered before any action about bundle is taken that 
223             // allows to trace all lifecycle changes
224             osgiContainerManager.addBundleListener(this);
225 
226             // We're pending installation, so install
227             final File file = pluginArtifact.toFile();
228             bundle = osgiContainerManager.installBundle(file, pluginArtifact.getReferenceMode());
229             bundleClassLoader = BundleClassLoaderAccessor.getClassLoader(bundle, new AlternativeDirectoryResourceLoader());
230             pkgAdminService = osgiContainerManager.getServiceTracker(PackageAdmin.class.getName());
231         } else if (null == bundle) {
232             throw new IllegalPluginStateException("Cannot reuse instance for bundle '" + getKey() + "'");
233         }
234         // else this could be a reinstall or not, but we can't tell, so we let it slide
235     }
236 
237     @Override
238     protected void uninstallInternal() {
239         try {
240             if (bundleIsUsable("uninstall")) {
241                 if (bundle.getState() != Bundle.UNINSTALLED) {
242                     if (null != osgiContainerManager && osgiContainerManager.isRunning()) {
243                         pkgAdminService.close();
244                         osgiContainerManager.removeBundleListener(this);
245                     } else {
246                         log.warn("OSGi container not running or undefined: Will not remove bundle listener and will not close package admin service");
247                     }
248                     bundle.uninstall();
249                 } else {
250                     // A previous uninstall aborted early ?
251                     log.warn("Bundle '{}' already UNINSTALLED, but still held", getKey());
252                 }
253                 bundle = null;
254                 bundleClassLoader = null;
255             }
256         } catch (final BundleException e) {
257             throw new PluginException(e);
258         }
259     }
260 
261     @Override
262     protected PluginState enableInternal() {
263         log.debug("Enabling OSGi bundled plugin '{}'", getKey());
264         try {
265             if (bundleIsUsable("enable")) {
266                 if (bundle.getHeaders().get(Constants.FRAGMENT_HOST) == null) {
267                     log.debug("Plugin '{}' bundle is NOT a fragment, starting.", getKey());
268                     // It is needs to give plugin time to launch Activator and register any services
269                     // before to move on. During activator work, plugin might register containerAccessor
270                     // service which will be used later during ModuleDescriptors activation. Note that it is not
271                     // necessary to bind any listeners here as at time when Bundle#start returns Bundle is already
272                     // in ACTIVE state
273                     setPluginState(PluginState.ENABLING);
274 
275                     // enable() might be caused by OSGi framework if some code starts underlaying bundle directly in
276                     // which case to synchronize Plugin state listener below calls enableInternal and it is necessary
277                     // to skip bundle activation to avoid deadlock/cycle
278                     if (bundle.getState() == Bundle.INSTALLED || bundle.getState() == Bundle.RESOLVED) {
279                         log.debug("Start plugin '{}' bundle", getKey());
280                         bundle.start();
281                     } else {
282                         log.debug("Skip plugin '{}' bundle start because of its state: {}", getKey(), bundle.getState());
283                     }
284 
285                     // Bundle moved to ACTIVE state, so it is time to take care of ContainerAccessor
286                     // IMPORTANT: ServiceTracker will handle ContainerAccessors provided by ALL bundles,
287                     // so special care should be taken to filter only services provided by the current bundle...
288                     //
289                     // TODO: Instead of service tracker it is possible to bind ServiceListener and fire
290                     // ContextRefresh family of events to be closer to how Spring staff works
291                     containerAccessorTracker = new ServiceTracker<>(
292                             bundle.getBundleContext(),
293                             ContainerAccessor.class,
294                             new ServiceTrackerCustomizer<ContainerAccessor, ContainerAccessor>() {
295 
296                                 @Override
297                                 public ContainerAccessor addingService(ServiceReference<ContainerAccessor> reference) {
298                                     if (reference.getBundle() == bundle) {
299                                         return bundle.getBundleContext().getService(reference);
300                                     }
301                                     return null;
302                                 }
303 
304                                 @Override
305                                 public void modifiedService(ServiceReference<ContainerAccessor> reference, ContainerAccessor service) {
306                                 }
307 
308                                 @Override
309                                 public void removedService(ServiceReference<ContainerAccessor> reference, ContainerAccessor service) {
310                                     if (reference.getBundle() == bundle) {
311                                         bundle.getBundleContext().ungetService(reference);
312                                     }
313                                 }
314 
315                             }
316                     );
317                     containerAccessorTracker.open();
318                 } else {
319                     log.debug("Plugin '{}' bundle is a fragment, not doing anything.", getKey());
320                 }
321             }
322             return PluginState.ENABLED;
323         } catch (final BundleException e) {
324             throw new PluginException(e);
325         }
326     }
327 
328     @Override
329     protected void disableInternal() {
330         try {
331             if (bundleIsUsable("disable")) {
332                 if (bundle.getState() == Bundle.ACTIVE) {
333                     // Should never be null for ACTIVE bundle
334                     if (containerAccessorTracker != null) {
335                         containerAccessorTracker.close();
336                     }
337                     bundle.stop();
338                 } else {
339                     log.warn("Cannot disable Bundle '{}', not ACTIVE", getKey());
340                 }
341             }
342         } catch (final BundleException e) {
343             throw new PluginException(e);
344         }
345     }
346 
347     @Override
348     public void bundleChanged(BundleEvent event) {
349         // Only events about current bundle are interesting
350         if (event.getBundle() != bundle) {
351             return;
352         }
353 
354         switch (event.getType()) {
355             // Bundle has been started. There are two possible scenarios how code could get here:
356             // 1. enable() has been called on plugin. In that case current state is ENABLING and no
357             //    action needs, transition to ENABLED will happens inside the method
358             // 2. bundle has been started by code outside of PluginsFramework. In that case plugin either
359             //    in INSTALLED or DISABLED state
360             case BundleEvent.STARTED:
361                 log.info("Plugin '{}' bundle started: {}", getKey(), getPluginState());
362                 if (getPluginState() != PluginState.ENABLING) {
363                     enable();
364                 }
365                 break;
366 
367             // Bundle has been stopped. There are two possible code patch that leads to that situation:
368             // 1. disable() was called. In that case nothing should be done, Plugin Framework proxy call and
369             //    takes care to update state accordingly
370             // 2. bundle has been stopped outside of Plugins Framework. In that case it is neccessary to synchronize
371             //    current Plugin state
372             case BundleEvent.STOPPED:
373                 log.info("Plugin '{}' bundle stopped: {}", getKey(), getPluginState());
374                 if (getPluginState() != PluginState.DISABLING) {
375                     disable();
376                 }
377                 break;
378         }
379     }
380 
381     @Override
382     public ContainerAccessor getContainerAccessor() {
383         // Check null on each call as service might come and go at any time
384         ContainerAccessor result = OsgiPluginUtil.createNonExistingPluginContainer(getKey());
385         if (containerAccessorTracker != null) {
386             ContainerAccessor tmp = containerAccessorTracker.getService();
387             if (tmp != null) {
388                 result = tmp;
389             }
390         }
391         return result;
392     }
393 
394     public ClassLoader getClassLoader() {
395         return getBundleClassLoaderOrFail();
396     }
397 
398     private String getInstallationStateExplanation() {
399         return (null != osgiContainerManager) ? "not yet installed" : "already uninstalled";
400     }
401 
402     private boolean bundleIsUsable(final String task) {
403         if (null != bundle) {
404             return true;
405         } else {
406             final String why = getInstallationStateExplanation();
407             log.warn("Cannot {} {} bundle '{}'", task, why, getKey());
408             return false;
409         }
410     }
411 
412     private <T> T getOrFail(final T what, final String name) throws PluginException {
413         if (null == what) {
414             throw new IllegalPluginStateException("Cannot use " + name + " of " + getInstallationStateExplanation() + " '"
415                     + getKey() + "' from '" + pluginArtifact + "'");
416         } else {
417             return what;
418         }
419     }
420 
421     private Bundle getBundleOrFail() throws PluginException {
422         return getOrFail(bundle, "bundle");
423     }
424 
425     private ClassLoader getBundleClassLoaderOrFail() throws PluginException {
426         return getOrFail(bundleClassLoader, "bundleClassLoader");
427     }
428 }