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.lang3.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() {
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                      ImmutableMap.<String, Class[]>builder()
71                              .putAll(beanInterfaces)
72                              .putAll(springHostComponentProviderConfig.getBeanInterfaces())
73                              .build(),
74                      ImmutableMap.<String, ContextClassLoaderStrategy>builder()
75                              .putAll(beanContextClassLoaderStrategies)
76                              .putAll(springHostComponentProviderConfig.getBeanContextClassLoaderStrategies())
77                              .build(),
78                      ImmutableSet.<String>builder()
79                              .addAll(bundleTrackingBeans)
80                              .addAll(springHostComponentProviderConfig.getBundleTrackingBeans())
81                              .build(),
82                      springHostComponentProviderConfig.isUseAnnotation());
83          }
84      }
85  
86      @Autowired(required = false)
87      public void setSpringHostComponentProviderConfig(SpringHostComponentProviderConfig springHostComponentProviderConfig) {
88          this.springHostComponentProviderConfig = springHostComponentProviderConfig;
89      }
90  
91      // These beanNames, beanInterfaces, beanContextClassLoaderStrategies only exist for keeping plugin framework's custom attributes during the parsing of spring context.
92      public void setBeanNames(Set<String> beanNames) {
93          this.beanNames = beanNames;
94      }
95  
96      public void setBeanInterfaces(Map<String, Class[]> beanInterfaces) {
97          this.beanInterfaces = beanInterfaces;
98      }
99  
100     public void setBeanContextClassLoaderStrategies(Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies) {
101         this.beanContextClassLoaderStrategies = beanContextClassLoaderStrategies;
102     }
103 
104     public void setBundleTrackingBeans(Set<String> bundleTrackingBeans) {
105         this.bundleTrackingBeans = bundleTrackingBeans;
106     }
107 
108     private static class SpringHostComponentProvider implements HostComponentProvider {
109         private final BeanFactory beanFactory;
110         private boolean useAnnotation;
111         private final Set<String> beanNames;
112         private final Map<String, Class[]> beanInterfaces;
113         private final Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies;
114         private final Set<String> bundleTrackingBeans;
115 
116         public SpringHostComponentProvider(BeanFactory beanFactory, Set<String> beanNames, Map<String, Class[]> beanInterfaces,
117                                            Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies,
118                                            Set<String> bundleTrackingBeans, boolean useAnnotation) {
119             this.beanFactory = notNull("beanFactory", beanFactory);
120             this.useAnnotation = useAnnotation;
121             this.beanNames = beanNames != null ? beanNames : new HashSet<>();
122             this.beanInterfaces = beanInterfaces != null ? beanInterfaces : new HashMap<>();
123             this.beanContextClassLoaderStrategies = beanContextClassLoaderStrategies != null ? beanContextClassLoaderStrategies : new HashMap<>();
124             this.bundleTrackingBeans = bundleTrackingBeans != null ? bundleTrackingBeans : new HashSet<>();
125         }
126 
127         public void provide(ComponentRegistrar registrar) {
128             final Set<String> beansToProvide = new HashSet<>(beanNames);
129             final Map<String, Class[]> interfacesToProvide = new HashMap<>(beanInterfaces);
130             final Map<String, ContextClassLoaderStrategy> contextClassLoaderStrategiesToProvide = new HashMap<>(beanContextClassLoaderStrategies);
131             final Set<String> bundleTrackingBeansToProvide = new HashSet<>(bundleTrackingBeans);
132 
133             if (useAnnotation) {
134                 scanForAnnotatedBeans(beansToProvide, interfacesToProvide, contextClassLoaderStrategiesToProvide, bundleTrackingBeansToProvide);
135             }
136 
137             provideBeans(registrar, beansToProvide, interfacesToProvide, contextClassLoaderStrategiesToProvide, bundleTrackingBeansToProvide);
138 
139             // make sure that host component providers we might have defined in parent bean factories also get a chance to provide their beans.
140             if (beanFactory instanceof HierarchicalBeanFactory) {
141                 final BeanFactory parentBeanFactory = ((HierarchicalBeanFactory) beanFactory).getParentBeanFactory();
142                 if (parentBeanFactory != null) {
143                     try {
144                         HostComponentProvider provider = (HostComponentProvider) parentBeanFactory.getBean(PluginBeanDefinitionRegistry.HOST_COMPONENT_PROVIDER);
145                         if (provider != null) {
146                             provider.provide(registrar);
147                         }
148                     } catch (NoSuchBeanDefinitionException e) {
149                         log.debug("Unable to find '" + PluginBeanDefinitionRegistry.HOST_COMPONENT_PROVIDER + "' in the parent bean factory " + parentBeanFactory);
150                     }
151                 }
152             }
153         }
154 
155         private void provideBeans(ComponentRegistrar registrar, Set<String> beanNames, Map<String, Class[]> beanInterfaces,
156                                   Map<String, ContextClassLoaderStrategy> beanContextClassLoaderStrategies, Set<String> bundleTrackingBeans) {
157             for (String beanName : beanNames) {
158                 if (beanFactory.isSingleton(beanName)) {
159                     final Object bean = beanFactory.getBean(beanName);
160                     Class[] interfaces = beanInterfaces.get(beanName);
161                     if (interfaces == null) {
162                         interfaces = findInterfaces(getBeanClass(bean));
163                     }
164                     registrar.register(interfaces)
165                             .forInstance(bean)
166                             .withName(beanName)
167                             .withContextClassLoaderStrategy(beanContextClassLoaderStrategies.getOrDefault(beanName, USE_HOST))
168                             .withTrackBundleEnabled(bundleTrackingBeans.contains(beanName));
169                 } else {
170                     log.warn("Cannot register bean '{}' as it's scope is not singleton", beanName);
171                 }
172             }
173         }
174 
175         /**
176          * The logic in this method is very similar to the logic in {@link AvailableToPluginsBeanDefinitionRegistryProcessor},
177          * but where this class works off the AvailableToPlugins annotation object directly, that class has to work off the
178          * lower-level MethodMetadata interface. If the logic in here is altered, then it should most likely be altered
179          * there also.
180          */
181         private void scanForAnnotatedBeans(Set<String> beansToProvide, Map<String, Class[]> interfacesToProvide,
182                                            Map<String, ContextClassLoaderStrategy> contextClassLoaderStrategiesToProvide,
183                                            Set<String> bundleTrackingBeansToProvide) {
184             if (beanFactory instanceof ListableBeanFactory) {
185                 for (String beanName : ((ListableBeanFactory) beanFactory).getBeanDefinitionNames()) {
186                     try {
187                         // Only singleton beans can be registered as service.
188                         if (beanFactory.isSingleton(beanName)) {
189                             final Class beanClass = getBeanClass(beanFactory.getBean(beanName));
190                             final AvailableToPlugins annotation = AnnotationUtils.findAnnotation(beanClass, AvailableToPlugins.class);
191                             if (annotation != null) {
192                                 beansToProvide.add(beanName);
193 
194                                 // if interface(s) is defined in the annotation
195                                 if (annotation.value() != Void.class || annotation.interfaces().length != 0) {
196                                     if (!interfacesToProvide.containsKey(beanName)) {
197                                         List<Class> effectiveInterfaces = new ArrayList<>();
198                                         if (annotation.value() != Void.class) {
199                                             effectiveInterfaces.add(annotation.value());
200                                         }
201 
202                                         if (annotation.interfaces().length != 0) {
203                                             effectiveInterfaces.addAll(Arrays.asList(annotation.interfaces()));
204                                         }
205 
206                                         interfacesToProvide.put(beanName, effectiveInterfaces.toArray(new Class[0]));
207                                     } else {
208                                         log.debug("Interfaces for bean '{}' have been defined in XML or in a Module definition, ignoring the interface defined in the annotation", beanName);
209                                     }
210                                 }
211 
212                                 if (!contextClassLoaderStrategiesToProvide.containsKey(beanName)) {
213                                     contextClassLoaderStrategiesToProvide.put(beanName, annotation.contextClassLoaderStrategy());
214                                 } else {
215                                     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);
216                                 }
217 
218                                 if (annotation.trackBundle()) {
219                                     bundleTrackingBeansToProvide.add(beanName);
220                                 }
221                             }
222                         } else {
223                             log.debug("Bean: " + beanName + " skipped during @AvailableToPlugins scanning since it's not a singleton bean");
224                         }
225                     } catch (BeanIsAbstractException ex) {
226                         // skipping abstract beans (is there a better way to check for this?)
227                     }
228                 }
229             } else {
230                 log.warn("Could not scan bean factory for beans to make available to plugins, bean factory is not 'listable'");
231             }
232         }
233 
234         private Class[] findInterfaces(Class cls) {
235             final List<Class> validInterfaces = new ArrayList<>();
236             for (Class inf : getAllInterfaces(cls)) {
237                 if (!inf.getName().startsWith("org.springframework")) {
238                     validInterfaces.add(inf);
239                 }
240             }
241             return validInterfaces.toArray(new Class[0]);
242         }
243 
244         private List<Class<?>> getAllInterfaces(Class cls) {
245             return ClassUtils.getAllInterfaces(cls);
246         }
247 
248         private Class getBeanClass(Object bean) {
249             return AopUtils.getTargetClass(bean);
250         }
251     }
252 }