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(), context.getInstallationMode()) && 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.") && !cls.startsWith("javax."))
238                 {
239                     Class spr;
240                     try
241                     {
242                         spr = ClassLoaderUtils.loadClass(cls, this.getClass());
243                     }
244                     catch (NoClassDefFoundError e)
245                     {
246                         // ignore class not found as it could be from another plugin
247                         continue;
248                     }
249                     catch (ClassNotFoundException e)
250                     {
251                         // ignore class not found as it could be from another plugin
252                         continue;
253                     }
254 
255                     // Search methods for parameters that use host components
256                     for (Method m : spr.getMethods())
257                     {
258                         for (Class param : m.getParameterTypes())
259                         {
260                             if (allHostComponents.contains(param.getName()))
261                             {
262                                 matchedHostComponents.add(param.getName());
263                             }
264                         }
265                     }
266                 }
267             }
268         }
269     }
270 
271     private List<String> findJarPaths(Manifest mf)
272     {
273         List<String> paths = new ArrayList<String>();
274         String cp = mf.getMainAttributes().getValue(Constants.BUNDLE_CLASSPATH);
275         if (cp != null)
276         {
277             for (String entry : cp.split(","))
278             {
279                 entry = entry.trim();
280                 if (entry.length() != 1 && entry.endsWith(".jar"))
281                 {
282                     paths.add(entry);
283                 }
284                 else if (!".".equals(entry))
285                 {
286                     log.warn("Non-jar classpath elements not supported: " + entry);
287                 }
288             }
289         }
290         return paths;
291     }
292 
293     /**
294      * Wrapper for the zip input stream to prevent clients from closing it when reading entries
295      */
296     private static class UnclosableFilterInputStream extends FilterInputStream
297     {
298         public UnclosableFilterInputStream(InputStream delegate)
299         {
300             super(delegate);
301         }
302 
303         @Override
304         public void close() throws IOException
305         {
306             // do nothing
307         }
308     }
309 
310     private String determineId(Set<String> hostComponentNames, String beanName, int iteration)
311     {
312         String id = beanName;
313         if (id == null)
314         {
315             id = "bean" + iteration;
316         }
317 
318         id = id.replaceAll("#", "LB");
319 
320         if (hostComponentNames.contains(id))
321         {
322             id += iteration;
323         }
324         return id;
325     }
326 
327     private boolean isRequiredHostComponent(TransformContext context, String name)
328     {
329         for (HostComponentRegistration registration : context.getRequiredHostComponents())
330         {
331             if (Arrays.asList(registration.getMainInterfaces()).contains(name))
332             {
333                 return true;
334             }
335         }
336         return false;
337     }
338 }