View Javadoc

1   package com.atlassian.plugin.spring;
2   
3   import com.atlassian.plugin.osgi.hostcomponents.ComponentRegistrar;
4   import com.atlassian.plugin.osgi.hostcomponents.ContextClassLoaderStrategy;
5   import com.atlassian.plugin.osgi.hostcomponents.HostComponentProvider;
6   import com.google.common.collect.ImmutableMap;
7   import com.google.common.collect.ImmutableSet;
8   import com.google.common.collect.Sets;
9   import org.apache.commons.lang.ClassUtils;
10  import org.slf4j.Logger;
11  import org.slf4j.LoggerFactory;
12  import org.springframework.aop.support.AopUtils;
13  import org.springframework.beans.factory.BeanFactory;
14  import org.springframework.beans.factory.BeanIsAbstractException;
15  import org.springframework.beans.factory.HierarchicalBeanFactory;
16  import org.springframework.beans.factory.ListableBeanFactory;
17  import org.springframework.beans.factory.NoSuchBeanDefinitionException;
18  import org.springframework.beans.factory.annotation.Autowired;
19  import org.springframework.beans.factory.config.AbstractFactoryBean;
20  import org.springframework.core.annotation.AnnotationUtils;
21  
22  import java.util.*;
23  
24  import static com.atlassian.plugin.osgi.hostcomponents.ContextClassLoaderStrategy.USE_HOST;
25  import static com.atlassian.plugin.util.Assertions.notNull;
26  
27  public class SpringHostComponentProviderFactoryBean extends AbstractFactoryBean
28  {
29      private static final Logger log = LoggerFactory.getLogger(SpringHostComponentProviderFactoryBean.class);
30  
31      private SpringHostComponentProviderConfig springHostComponentProviderConfig;
32  
33      /**
34       * A set of bean names to make available to plugins
35       */
36      private Set<String> beanNames = Collections.emptySet();
37  
38      /**
39       * Mapping of beanNames to the interfaces it should be exposed as. Note that if a bean name is present an no interface
40       * is defined then all its interfaces should be 'exposed'.
41       */
42      private Map<String, Class[]> beanInterfaces = Collections.emptyMap();
43  
44      /**
45       * Mapping of beanNames with their {@link com.atlassian.plugin.osgi.hostcomponents.ContextClassLoaderStrategy}.
46       * Default value is {@link com.atlassian.plugin.osgi.hostcomponents.ContextClassLoaderStrategy#USE_HOST}.
47       */
48      private Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies = Collections.emptyMap();
49  
50      private Set<String> bundleTrackingBeans = Collections.emptySet();
51  
52      public Class getObjectType()
53      {
54          return HostComponentProvider.class;
55      }
56  
57      protected Object createInstance() throws Exception
58      {
59          if (springHostComponentProviderConfig == null)
60          {
61              return new SpringHostComponentProvider(getBeanFactory(), beanNames, beanInterfaces,
62                      beanContextClassLoaderStrategies, bundleTrackingBeans, false);
63          }
64          else
65          {
66              // whatever defined in {@link SpringHostComponentProviderConfig} takes precedence
67              return new SpringHostComponentProvider(getBeanFactory(),
68                      Sets.union(beanNames, springHostComponentProviderConfig.getBeanNames()),
69                      new ImmutableMap.Builder().putAll(beanInterfaces).putAll(springHostComponentProviderConfig.getBeanInterfaces()).build(),
70                      new ImmutableMap.Builder().putAll(beanContextClassLoaderStrategies).putAll(springHostComponentProviderConfig.getBeanContextClassLoaderStrategies()).build(),
71                      new ImmutableSet.Builder().addAll(bundleTrackingBeans).addAll(springHostComponentProviderConfig.getBundleTrackingBeans()).build(),
72                      springHostComponentProviderConfig.isUseAnnotation());
73          }
74      }
75  
76      @Autowired(required = false)
77      public void setSpringHostComponentProviderConfig(SpringHostComponentProviderConfig springHostComponentProviderConfig)
78      {
79          this.springHostComponentProviderConfig = springHostComponentProviderConfig;
80      }
81  
82      // These beanNames, beanInterfaces, beanContextClassLoaderStrategies only exist for keeping plugin framework's custom attributes during the parsing of spring context.
83      public void setBeanNames(Set<String> beanNames)
84      {
85          this.beanNames = beanNames;
86      }
87  
88      public void setBeanInterfaces(Map<String, Class[]> beanInterfaces)
89      {
90          this.beanInterfaces = beanInterfaces;
91      }
92  
93      public void setBeanContextClassLoaderStrategies(Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies)
94      {
95          this.beanContextClassLoaderStrategies = beanContextClassLoaderStrategies;
96      }
97  
98      public void setBundleTrackingBeans(Set<String> bundleTrackingBeans)
99      {
100         this.bundleTrackingBeans = bundleTrackingBeans;
101     }
102 
103     private static class SpringHostComponentProvider implements HostComponentProvider
104     {
105         private final BeanFactory beanFactory;
106         private boolean useAnnotation;
107         private final Set<String> beanNames;
108         private final Map<String, Class[]> beanInterfaces;
109         private final Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies;
110         private final Set<String> bundleTrackingBeans;
111 
112         public SpringHostComponentProvider(BeanFactory beanFactory, Set<String> beanNames, Map<String, Class[]> beanInterfaces,
113                                            Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies,
114                                            Set<String> bundleTrackingBeans, boolean useAnnotation)
115         {
116             this.beanFactory = notNull("beanFactory", beanFactory);
117             this.useAnnotation = useAnnotation;
118             this.beanNames = beanNames != null ? beanNames : new HashSet<String>();
119             this.beanInterfaces = beanInterfaces != null ? beanInterfaces : new HashMap<String, Class[]>();
120             this.beanContextClassLoaderStrategies = beanContextClassLoaderStrategies != null ? beanContextClassLoaderStrategies : new HashMap<String, ContextClassLoaderStrategy>();
121             this.bundleTrackingBeans = bundleTrackingBeans != null ? bundleTrackingBeans : new HashSet<String>();
122         }
123 
124         public void provide(ComponentRegistrar registrar)
125         {
126             final Set<String> beansToProvide = new HashSet<String>(beanNames);
127             final Map<String, Class[]> interfacesToProvide = new HashMap<String, Class[]>(beanInterfaces);
128             final Map<String, ContextClassLoaderStrategy> contextClassLoaderStrategiesToProvide = new HashMap<String, ContextClassLoaderStrategy>(beanContextClassLoaderStrategies);
129             final Set<String> bundleTrackingBeansToProvide = new HashSet<String>(bundleTrackingBeans);
130 
131             if (useAnnotation)
132             {
133                 scanForAnnotatedBeans(beansToProvide, interfacesToProvide, contextClassLoaderStrategiesToProvide, bundleTrackingBeansToProvide);
134             }
135 
136             provideBeans(registrar, beansToProvide, interfacesToProvide, contextClassLoaderStrategiesToProvide, bundleTrackingBeansToProvide);
137 
138             // make sure that host component providers we might have defined in parent bean factories also get a chance to provide their beans.
139             if (beanFactory instanceof HierarchicalBeanFactory)
140             {
141                 final BeanFactory parentBeanFactory = ((HierarchicalBeanFactory) beanFactory).getParentBeanFactory();
142                 if (parentBeanFactory != null)
143                 {
144                     try
145                     {
146                         HostComponentProvider provider = (HostComponentProvider) parentBeanFactory.getBean(PluginBeanDefinitionRegistry.HOST_COMPONENT_PROVIDER);
147                         if (provider != null)
148                         {
149                             provider.provide(registrar);
150                         }
151                     }
152                     catch (NoSuchBeanDefinitionException e)
153                     {
154                         log.debug("Unable to find '" + PluginBeanDefinitionRegistry.HOST_COMPONENT_PROVIDER + "' in the parent bean factory " + parentBeanFactory);
155                     }
156                 }
157             }
158         }
159 
160         private void provideBeans(ComponentRegistrar registrar, Set<String> beanNames, Map<String, Class[]> beanInterfaces,
161                                   Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies, Set<String> bundleTrackingBeans)
162         {
163             for (String beanName : beanNames)
164             {
165                 if (beanFactory.isSingleton(beanName))
166                 {
167                     final Object bean = beanFactory.getBean(beanName);
168                     Class[] interfaces = beanInterfaces.get(beanName);
169                     if (interfaces == null)
170                     {
171                         interfaces = findInterfaces(getBeanClass(bean));
172                     }
173                     registrar.register(interfaces)
174                             .forInstance(bean)
175                             .withName(beanName)
176                             .withContextClassLoaderStrategy(beanContextClassLoaderStrategies.containsKey(beanName) ? beanContextClassLoaderStrategies.get(beanName) : USE_HOST)
177                             .withTrackBundleEnabled(bundleTrackingBeans.contains(beanName));
178                 }
179                 else
180                 {
181                     log.warn("Cannot register bean '{}' as it's scope is not singleton", beanName);
182                 }
183             }
184         }
185 
186         private void scanForAnnotatedBeans(Set<String> beansToProvide, Map<String, Class[]> interfacesToProvide,
187                                            Map<String, ContextClassLoaderStrategy> contextClassLoaderStrategiesToProvide,
188                                            Set<String> bundleTrackingBeansToProvide)
189         {
190             if (beanFactory instanceof ListableBeanFactory)
191             {
192                 for (String beanName : ((ListableBeanFactory) beanFactory).getBeanDefinitionNames())
193                 {
194                     try
195                     {
196                         // Only singleton beans can be registered as service.
197                         if (beanFactory.isSingleton(beanName))
198                         {
199                             final Class beanClass = getBeanClass(beanFactory.getBean(beanName));
200                             final AvailableToPlugins annotation = AnnotationUtils.findAnnotation(beanClass, AvailableToPlugins.class);
201                             if (annotation != null)
202                             {
203                                 beansToProvide.add(beanName);
204 
205                                 // if interface(s) is defined in the annotation
206                                 if (annotation.value() != Void.class || annotation.interfaces().length != 0)
207                                 {
208                                     if (!interfacesToProvide.containsKey(beanName))
209                                     {
210                                         List<Class> effectiveInterfaces = new ArrayList<Class>();
211                                         if (annotation.value() != Void.class)
212                                         {
213                                             effectiveInterfaces.add(annotation.value());
214                                         }
215 
216                                         if (annotation.interfaces().length != 0)
217                                         {
218                                             effectiveInterfaces.addAll(Arrays.asList(annotation.interfaces()));
219                                         }
220 
221                                         interfacesToProvide.put(beanName, effectiveInterfaces.toArray(new Class[0]));
222                                     }
223                                     else
224                                     {
225                                         log.debug("Interfaces for bean '{}' have been defined in XML or in a Module definition, ignoring the interface defined in the annotation", beanName);
226                                     }
227                                 }
228 
229                                 if (!contextClassLoaderStrategiesToProvide.containsKey(beanName))
230                                 {
231                                     contextClassLoaderStrategiesToProvide.put(beanName, annotation.contextClassLoaderStrategy());
232                                 }
233                                 else
234                                 {
235                                     log.debug("Context class loader strategy for bean '{}' has been defined in XML or in a Module definition, ignoring the one defined in the annotation", beanName);
236                                 }
237 
238                                 if (annotation.trackBundle())
239                                 {
240                                     bundleTrackingBeansToProvide.add(beanName);
241                                 }
242                             }
243                         }
244                         else
245                         {
246                             log.debug("Bean: " + beanName + " skipped during @AvailableToPlugins scanning since it's not a singleton bean");
247                         }
248                     }
249                     catch (BeanIsAbstractException ex)
250                     {
251                         // skipping abstract beans (is there a better way to check for this?)
252                     }
253                 }
254             }
255             else
256             {
257                 log.warn("Could not scan bean factory for beans to make available to plugins, bean factory is not 'listable'");
258             }
259         }
260 
261         private Class[] findInterfaces(Class cls)
262         {
263             final List<Class> validInterfaces = new ArrayList<Class>();
264             for (Class inf : getAllInterfaces(cls))
265             {
266                 if (!inf.getName().startsWith("org.springframework"))
267                 {
268                     validInterfaces.add(inf);
269                 }
270             }
271             return validInterfaces.toArray(new Class[validInterfaces.size()]);
272         }
273 
274         @SuppressWarnings("unchecked")
275         private List<Class> getAllInterfaces(Class cls)
276         {
277             return (List<Class>) ClassUtils.getAllInterfaces(cls);
278         }
279 
280         private Class getBeanClass(Object bean)
281         {
282             return AopUtils.getTargetClass(bean);
283         }
284     }
285 }