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.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  
31  import static com.atlassian.plugin.osgi.hostcomponents.ContextClassLoaderStrategy.USE_HOST;
32  import static com.atlassian.plugin.util.Assertions.notNull;
33  
34  public class SpringHostComponentProviderFactoryBean extends AbstractFactoryBean {
35      private static final Logger log = LoggerFactory.getLogger(SpringHostComponentProviderFactoryBean.class);
36  
37      private SpringHostComponentProviderConfig springHostComponentProviderConfig;
38  
39      /**
40       * A set of bean names to make available to plugins
41       */
42      private Set<String> beanNames = Collections.emptySet();
43  
44      /**
45       * Mapping of beanNames to the interfaces it should be exposed as. Note that if a bean name is present an no interface
46       * is defined then all its interfaces should be 'exposed'.
47       */
48      private Map<String, Class[]> beanInterfaces = Collections.emptyMap();
49  
50      /**
51       * Mapping of beanNames with their {@link com.atlassian.plugin.osgi.hostcomponents.ContextClassLoaderStrategy}.
52       * Default value is {@link com.atlassian.plugin.osgi.hostcomponents.ContextClassLoaderStrategy#USE_HOST}.
53       */
54      private Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies = Collections.emptyMap();
55  
56      private Set<String> bundleTrackingBeans = Collections.emptySet();
57  
58      public Class getObjectType() {
59          return HostComponentProvider.class;
60      }
61  
62      protected Object createInstance() throws Exception {
63          if (springHostComponentProviderConfig == null) {
64              return new SpringHostComponentProvider(getBeanFactory(), beanNames, beanInterfaces,
65                      beanContextClassLoaderStrategies, bundleTrackingBeans, false);
66          } else {
67              // whatever defined in {@link SpringHostComponentProviderConfig} takes precedence
68              return new SpringHostComponentProvider(getBeanFactory(),
69                      Sets.union(beanNames, springHostComponentProviderConfig.getBeanNames()),
70                      new ImmutableMap.Builder().putAll(beanInterfaces).putAll(springHostComponentProviderConfig.getBeanInterfaces()).build(),
71                      new ImmutableMap.Builder().putAll(beanContextClassLoaderStrategies).putAll(springHostComponentProviderConfig.getBeanContextClassLoaderStrategies()).build(),
72                      new ImmutableSet.Builder().addAll(bundleTrackingBeans).addAll(springHostComponentProviderConfig.getBundleTrackingBeans()).build(),
73                      springHostComponentProviderConfig.isUseAnnotation());
74          }
75      }
76  
77      @Autowired(required = false)
78      public void setSpringHostComponentProviderConfig(SpringHostComponentProviderConfig springHostComponentProviderConfig) {
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          this.beanNames = beanNames;
85      }
86  
87      public void setBeanInterfaces(Map<String, Class[]> beanInterfaces) {
88          this.beanInterfaces = beanInterfaces;
89      }
90  
91      public void setBeanContextClassLoaderStrategies(Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies) {
92          this.beanContextClassLoaderStrategies = beanContextClassLoaderStrategies;
93      }
94  
95      public void setBundleTrackingBeans(Set<String> bundleTrackingBeans) {
96          this.bundleTrackingBeans = bundleTrackingBeans;
97      }
98  
99      private static class SpringHostComponentProvider implements HostComponentProvider {
100         private final BeanFactory beanFactory;
101         private boolean useAnnotation;
102         private final Set<String> beanNames;
103         private final Map<String, Class[]> beanInterfaces;
104         private final Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies;
105         private final Set<String> bundleTrackingBeans;
106 
107         public SpringHostComponentProvider(BeanFactory beanFactory, Set<String> beanNames, Map<String, Class[]> beanInterfaces,
108                                            Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies,
109                                            Set<String> bundleTrackingBeans, boolean useAnnotation) {
110             this.beanFactory = notNull("beanFactory", beanFactory);
111             this.useAnnotation = useAnnotation;
112             this.beanNames = beanNames != null ? beanNames : new HashSet<String>();
113             this.beanInterfaces = beanInterfaces != null ? beanInterfaces : new HashMap<String, Class[]>();
114             this.beanContextClassLoaderStrategies = beanContextClassLoaderStrategies != null ? beanContextClassLoaderStrategies : new HashMap<String, ContextClassLoaderStrategy>();
115             this.bundleTrackingBeans = bundleTrackingBeans != null ? bundleTrackingBeans : new HashSet<String>();
116         }
117 
118         public void provide(ComponentRegistrar registrar) {
119             final Set<String> beansToProvide = new HashSet<String>(beanNames);
120             final Map<String, Class[]> interfacesToProvide = new HashMap<String, Class[]>(beanInterfaces);
121             final Map<String, ContextClassLoaderStrategy> contextClassLoaderStrategiesToProvide = new HashMap<String, ContextClassLoaderStrategy>(beanContextClassLoaderStrategies);
122             final Set<String> bundleTrackingBeansToProvide = new HashSet<String>(bundleTrackingBeans);
123 
124             if (useAnnotation) {
125                 scanForAnnotatedBeans(beansToProvide, interfacesToProvide, contextClassLoaderStrategiesToProvide, bundleTrackingBeansToProvide);
126             }
127 
128             provideBeans(registrar, beansToProvide, interfacesToProvide, contextClassLoaderStrategiesToProvide, bundleTrackingBeansToProvide);
129 
130             // make sure that host component providers we might have defined in parent bean factories also get a chance to provide their beans.
131             if (beanFactory instanceof HierarchicalBeanFactory) {
132                 final BeanFactory parentBeanFactory = ((HierarchicalBeanFactory) beanFactory).getParentBeanFactory();
133                 if (parentBeanFactory != null) {
134                     try {
135                         HostComponentProvider provider = (HostComponentProvider) parentBeanFactory.getBean(PluginBeanDefinitionRegistry.HOST_COMPONENT_PROVIDER);
136                         if (provider != null) {
137                             provider.provide(registrar);
138                         }
139                     } catch (NoSuchBeanDefinitionException e) {
140                         log.debug("Unable to find '" + PluginBeanDefinitionRegistry.HOST_COMPONENT_PROVIDER + "' in the parent bean factory " + parentBeanFactory);
141                     }
142                 }
143             }
144         }
145 
146         private void provideBeans(ComponentRegistrar registrar, Set<String> beanNames, Map<String, Class[]> beanInterfaces,
147                                   Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies, Set<String> bundleTrackingBeans) {
148             for (String beanName : beanNames) {
149                 if (beanFactory.isSingleton(beanName)) {
150                     final Object bean = beanFactory.getBean(beanName);
151                     Class[] interfaces = beanInterfaces.get(beanName);
152                     if (interfaces == null) {
153                         interfaces = findInterfaces(getBeanClass(bean));
154                     }
155                     registrar.register(interfaces)
156                             .forInstance(bean)
157                             .withName(beanName)
158                             .withContextClassLoaderStrategy(beanContextClassLoaderStrategies.containsKey(beanName) ? beanContextClassLoaderStrategies.get(beanName) : USE_HOST)
159                             .withTrackBundleEnabled(bundleTrackingBeans.contains(beanName));
160                 } else {
161                     log.warn("Cannot register bean '{}' as it's scope is not singleton", beanName);
162                 }
163             }
164         }
165 
166         /**
167          * The logic in this method is very similar to the logic in {@link AvailableToPluginsBeanDefinitionRegistryProcessor},
168          * but where this class works off the AvailableToPlugins annotation object directly, that class has to work off the
169          * lower-level MethodMetadata interface. If the logic in here is altered, then it should most likely be altered
170          * there also.
171          */
172         private void scanForAnnotatedBeans(Set<String> beansToProvide, Map<String, Class[]> interfacesToProvide,
173                                            Map<String, ContextClassLoaderStrategy> contextClassLoaderStrategiesToProvide,
174                                            Set<String> bundleTrackingBeansToProvide) {
175             if (beanFactory instanceof ListableBeanFactory) {
176                 for (String beanName : ((ListableBeanFactory) beanFactory).getBeanDefinitionNames()) {
177                     try {
178                         // Only singleton beans can be registered as service.
179                         if (beanFactory.isSingleton(beanName)) {
180                             final Class beanClass = getBeanClass(beanFactory.getBean(beanName));
181                             final AvailableToPlugins annotation = AnnotationUtils.findAnnotation(beanClass, AvailableToPlugins.class);
182                             if (annotation != null) {
183                                 beansToProvide.add(beanName);
184 
185                                 // if interface(s) is defined in the annotation
186                                 if (annotation.value() != Void.class || annotation.interfaces().length != 0) {
187                                     if (!interfacesToProvide.containsKey(beanName)) {
188                                         List<Class> effectiveInterfaces = new ArrayList<Class>();
189                                         if (annotation.value() != Void.class) {
190                                             effectiveInterfaces.add(annotation.value());
191                                         }
192 
193                                         if (annotation.interfaces().length != 0) {
194                                             effectiveInterfaces.addAll(Arrays.asList(annotation.interfaces()));
195                                         }
196 
197                                         interfacesToProvide.put(beanName, effectiveInterfaces.toArray(new Class[0]));
198                                     } else {
199                                         log.debug("Interfaces for bean '{}' have been defined in XML or in a Module definition, ignoring the interface defined in the annotation", beanName);
200                                     }
201                                 }
202 
203                                 if (!contextClassLoaderStrategiesToProvide.containsKey(beanName)) {
204                                     contextClassLoaderStrategiesToProvide.put(beanName, annotation.contextClassLoaderStrategy());
205                                 } else {
206                                     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);
207                                 }
208 
209                                 if (annotation.trackBundle()) {
210                                     bundleTrackingBeansToProvide.add(beanName);
211                                 }
212                             }
213                         } else {
214                             log.debug("Bean: " + beanName + " skipped during @AvailableToPlugins scanning since it's not a singleton bean");
215                         }
216                     } catch (BeanIsAbstractException ex) {
217                         // skipping abstract beans (is there a better way to check for this?)
218                     }
219                 }
220             } else {
221                 log.warn("Could not scan bean factory for beans to make available to plugins, bean factory is not 'listable'");
222             }
223         }
224 
225         private Class[] findInterfaces(Class cls) {
226             final List<Class> validInterfaces = new ArrayList<Class>();
227             for (Class inf : getAllInterfaces(cls)) {
228                 if (!inf.getName().startsWith("org.springframework")) {
229                     validInterfaces.add(inf);
230                 }
231             }
232             return validInterfaces.toArray(new Class[validInterfaces.size()]);
233         }
234 
235         @SuppressWarnings("unchecked")
236         private List<Class> getAllInterfaces(Class cls) {
237             return (List<Class>) ClassUtils.getAllInterfaces(cls);
238         }
239 
240         private Class getBeanClass(Object bean) {
241             return AopUtils.getTargetClass(bean);
242         }
243     }
244 }