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