View Javadoc

1   package com.atlassian.plugin.osgi.factory.transform.stage;
2   
3   import aQute.lib.osgi.Clazz;
4   import com.atlassian.plugin.osgi.factory.transform.TransformStage;
5   import com.atlassian.plugin.osgi.factory.transform.TransformContext;
6   import com.atlassian.plugin.osgi.factory.transform.PluginTransformationException;
7   import com.atlassian.plugin.osgi.factory.transform.model.ComponentImport;
8   import com.atlassian.plugin.osgi.factory.transform.model.SystemExports;
9   import com.atlassian.plugin.osgi.hostcomponents.HostComponentRegistration;
10  import com.atlassian.plugin.osgi.hostcomponents.PropertyBuilder;
11  import com.atlassian.plugin.osgi.hostcomponents.ComponentRegistrar;
12  import com.atlassian.plugin.osgi.util.ClassBinaryScanner;
13  import com.atlassian.plugin.osgi.util.OsgiHeaderUtil;
14  import com.atlassian.plugin.PluginParseException;
15  import com.atlassian.plugin.util.ClassLoaderUtils;
16  import com.atlassian.plugin.util.PluginUtils;
17  import com.atlassian.plugin.osgi.util.ClassBinaryScanner.InputStreamResource;
18  import com.atlassian.plugin.osgi.util.ClassBinaryScanner.ScanResult;
19  import org.dom4j.Document;
20  import org.dom4j.Element;
21  import org.apache.commons.io.IOUtils;
22  import org.osgi.framework.Constants;
23  import org.slf4j.Logger;
24  import org.slf4j.LoggerFactory;
25  
26  import java.io.BufferedInputStream;
27  import java.io.FilterInputStream;
28  import java.util.*;
29  import java.util.jar.Manifest;
30  import java.util.zip.ZipInputStream;
31  import java.util.zip.ZipEntry;
32  import java.io.FileInputStream;
33  import java.io.IOException;
34  import java.io.InputStream;
35  import java.lang.reflect.Method;
36  
37  public class HostComponentSpringStage implements TransformStage
38  {
39      private static final Logger log = LoggerFactory.getLogger(HostComponentSpringStage.class);
40  
41      /** Path of generated Spring XML file */
42      private static final String SPRING_XML = "META-INF/spring/atlassian-plugins-host-components.xml";
43  
44      public static final String BEAN_SOURCE = "Host Component";
45  
46      public void execute(TransformContext context) throws PluginTransformationException
47      {
48          if (SpringHelper.shouldGenerateFile(context, SPRING_XML))
49          {
50              Document doc = SpringHelper.createSpringDocument();
51              Set<String> hostComponentInterfaceNames = convertRegistrationsToSet(context.getHostComponentRegistrations());
52              Set<String> matchedInterfaceNames = new HashSet<String>();
53              List<String> innerJarPaths = findJarPaths(context.getManifest());
54              InputStream pluginStream = null;
55              try
56              {
57                  pluginStream = new FileInputStream(context.getPluginFile());
58                  findUsedHostComponents(hostComponentInterfaceNames, matchedInterfaceNames, innerJarPaths, pluginStream);
59              }
60              catch (IOException e)
61              {
62                  throw new PluginParseException("Unable to scan for host components in plugin classes", e);
63              }
64              finally
65              {
66                  IOUtils.closeQuietly(pluginStream);
67              }
68  
69              List<HostComponentRegistration> matchedRegistrations = new ArrayList<HostComponentRegistration>();
70              Element root = doc.getRootElement();
71              if (context.getHostComponentRegistrations() != null)
72              {
73                  int index = -1;
74                  for (HostComponentRegistration reg : context.getHostComponentRegistrations())
75                  {
76                      index++;
77                      boolean found = false;
78                      for (String name : reg.getMainInterfaces())
79                      {
80                          if (matchedInterfaceNames.contains(name) || isRequiredHostComponent(context, name))
81                          {
82                              found = true;
83                          }
84                      }
85                      Set<String> regInterfaces = new HashSet<String>(Arrays.asList(reg.getMainInterfaces()));
86                      for (ComponentImport compImport : context.getComponentImports().values())
87                      {
88                          if (PluginUtils.doesModuleElementApplyToApplication(compImport.getSource(), context.getApplications()) && regInterfaces.containsAll(compImport.getInterfaces()))
89                          {
90                              found = false;
91                              break;
92                          }
93                      }
94  
95                      if (!found)
96                      {
97                          continue;
98                      }
99                      matchedRegistrations.add(reg);
100 
101                     String beanName = reg.getProperties().get(PropertyBuilder.BEAN_NAME);
102 
103                     // We don't use Spring DM service references here, because when the plugin is disabled, the proxies
104                     // will be marked destroyed, causing undesirable ServiceProxyDestroyedException fireworks.  Since we
105                     // know host components won't change over the runtime of the plugin, we can use a simple factory
106                     // bean that returns the actual component instance
107 
108                     Element osgiService = root.addElement("beans:bean");
109                     String beanId = determineId(context.getComponentImports().keySet(), beanName, index);
110 
111                     // make sure the new bean id is not already in use.
112                     context.trackBean(beanId, BEAN_SOURCE);
113 
114                     osgiService.addAttribute("id", beanId);
115                     osgiService.addAttribute("lazy-init", "true");
116 
117                     // These are strings since we aren't compiling against the osgi-bridge jar
118                     osgiService.addAttribute("class", "com.atlassian.plugin.osgi.bridge.external.HostComponentFactoryBean");
119                     context.getExtraImports().add("com.atlassian.plugin.osgi.bridge.external");
120 
121                     Element e = osgiService.addElement("beans:property");
122                     e.addAttribute("name", "filter");
123 
124                     e.addAttribute("value", "(&(bean-name=" + beanName + ")(" + ComponentRegistrar.HOST_COMPONENT_FLAG + "=true))");
125 
126                     Element listProp = osgiService.addElement("beans:property");
127                     listProp.addAttribute("name", "interfaces");
128                     Element list = listProp.addElement("beans:list");
129                     for (String inf : reg.getMainInterfaces())
130                     {
131                         Element tmp = list.addElement("beans:value");
132                         tmp.setText(inf);
133                     }
134                 }
135             }
136             addImportsForMatchedHostComponents(matchedRegistrations, context.getSystemExports(), context.getExtraImports());
137             if (root.elements().size() > 0)
138             {
139                 context.setShouldRequireSpring(true);
140                 context.getFileOverrides().put(SPRING_XML, SpringHelper.documentToBytes(doc));
141             }
142         }
143     }
144 
145     private void addImportsForMatchedHostComponents(List<HostComponentRegistration> matchedRegistrations,
146                                                     SystemExports systemExports, List<String> extraImports)
147     {
148         try
149         {
150             Set<String> referredPackages = OsgiHeaderUtil.findReferredPackageNames(matchedRegistrations);
151 
152             for (String pkg:referredPackages)
153             {
154                 extraImports.add(systemExports.getFullExport(pkg));
155             }
156         }
157         catch (IOException e)
158         {
159             throw new PluginTransformationException("Unable to scan for host component referred packages", e);
160         }
161     }
162 
163 
164     private Set<String> convertRegistrationsToSet(List<HostComponentRegistration> regs)
165     {
166         Set<String> interfaceNames = new HashSet<String>();
167         if (regs != null)
168         {
169             for (HostComponentRegistration reg : regs)
170             {
171                 interfaceNames.addAll(Arrays.asList(reg.getMainInterfaces()));
172             }
173         }
174         return interfaceNames;
175     }
176 
177     private void findUsedHostComponents(Set<String> allHostComponents, Set<String> matchedHostComponents, List<String> innerJarPaths, InputStream
178             jarStream) throws IOException
179     {
180         Set<String> entries = new HashSet<String>();
181         Set<String> superClassNames = new HashSet<String>();
182         ZipInputStream zin = null;
183         try
184         {
185             zin = new ZipInputStream(new BufferedInputStream(jarStream));
186             ZipEntry zipEntry;
187             while ((zipEntry = zin.getNextEntry()) != null)
188             {
189                 String path = zipEntry.getName();
190                 if (path.endsWith(".class"))
191                 {
192                     entries.add(path.substring(0, path.length() - ".class".length()));
193                     final Clazz cls = new Clazz(path, new InputStreamResource(new BufferedInputStream(new UnclosableFilterInputStream(zin))));
194                     final ScanResult scanResult = ClassBinaryScanner.scanClassBinary(cls);
195 
196                     superClassNames.add(scanResult.getSuperClass());
197                     for (String ref : scanResult.getReferredClasses())
198                     {
199                         String name = TransformStageUtils.jarPathToClassName(ref + ".class");
200                         if (allHostComponents.contains(name))
201                         {
202                             matchedHostComponents.add(name);
203                         }
204                     }
205                 }
206                 else if (path.endsWith(".jar") && innerJarPaths.contains(path))
207                 {
208                     findUsedHostComponents(allHostComponents, matchedHostComponents, Collections.<String>emptyList(), new UnclosableFilterInputStream(zin));
209                 }
210             }
211         }
212         finally
213         {
214             IOUtils.closeQuietly(zin);
215         }
216 
217         addHostComponentsUsedInSuperClasses(allHostComponents, matchedHostComponents, entries, superClassNames);
218     }
219 
220     /**
221      * Searches super classes not in the plugin jar, which have methods that use host components
222      * @param allHostComponents The set of all host component classes
223      * @param matchedHostComponents The set of host component classes already found
224      * @param entries The paths of all files in the jar
225      * @param superClassNames All super classes find by classes in the jar
226      */
227     private void addHostComponentsUsedInSuperClasses(Set<String> allHostComponents, Set<String> matchedHostComponents, Set<String> entries, Set<String> superClassNames)
228     {
229         for (String superClassName : superClassNames)
230         {
231             // Only search super classes not in the jar
232             if (!entries.contains(superClassName))
233             {
234                 String cls = superClassName.replace('/','.');
235 
236                 // Ignore java classes including Object
237                 if (!cls.startsWith("java."))
238                 {
239                     Class spr;
240                     try
241                     {
242                         spr = ClassLoaderUtils.loadClass(cls, this.getClass());
243                     }
244                     catch (ClassNotFoundException e)
245                     {
246                         // ignore class not found as it could be from another plugin
247                         continue;
248                     }
249 
250                     // Search methods for parameters that use host components
251                     for (Method m : spr.getMethods())
252                     {
253                         for (Class param : m.getParameterTypes())
254                         {
255                             if (allHostComponents.contains(param.getName()))
256                             {
257                                 matchedHostComponents.add(param.getName());
258                             }
259                         }
260                     }
261                 }
262             }
263         }
264     }
265 
266     private List<String> findJarPaths(Manifest mf)
267     {
268         List<String> paths = new ArrayList<String>();
269         String cp = mf.getMainAttributes().getValue(Constants.BUNDLE_CLASSPATH);
270         if (cp != null)
271         {
272             for (String entry : cp.split(","))
273             {
274                 entry = entry.trim();
275                 if (entry.length() != 1 && entry.endsWith(".jar"))
276                 {
277                     paths.add(entry);
278                 }
279                 else if (!".".equals(entry))
280                 {
281                     log.warn("Non-jar classpath elements not supported: " + entry);
282                 }
283             }
284         }
285         return paths;
286     }
287 
288     /**
289      * Wrapper for the zip input stream to prevent clients from closing it when reading entries
290      */
291     private static class UnclosableFilterInputStream extends FilterInputStream
292     {
293         public UnclosableFilterInputStream(InputStream delegate)
294         {
295             super(delegate);
296         }
297 
298         @Override
299         public void close() throws IOException
300         {
301             // do nothing
302         }
303     }
304 
305     private String determineId(Set<String> hostComponentNames, String beanName, int iteration)
306     {
307         String id = beanName;
308         if (id == null)
309         {
310             id = "bean" + iteration;
311         }
312 
313         id = id.replaceAll("#", "LB");
314 
315         if (hostComponentNames.contains(id))
316         {
317             id += iteration;
318         }
319         return id;
320     }
321 
322     private boolean isRequiredHostComponent(TransformContext context, String name)
323     {
324         for (HostComponentRegistration registration : context.getRequiredHostComponents())
325         {
326             if (Arrays.asList(registration.getMainInterfaces()).contains(name))
327             {
328                 return true;
329             }
330         }
331         return false;
332     }
333 }