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                     e.addAttribute("value", "(&(bean-name=" + beanName + ")(" + ComponentRegistrar.HOST_COMPONENT_FLAG + "=true))");
124 
125                     Element listProp = osgiService.addElement("beans:property");
126                     listProp.addAttribute("name", "interfaces");
127                     Element list = listProp.addElement("beans:list");
128                     for (String inf : reg.getMainInterfaces())
129                     {
130                         Element tmp = list.addElement("beans:value");
131                         tmp.setText(inf);
132                     }
133 
134                     Element bundleContextProp = osgiService.addElement("beans:property");
135                     bundleContextProp.addAttribute("name", "bundleContext");
136                     bundleContextProp.addAttribute("ref", "bundleContext");
137                 }
138             }
139             addImportsForMatchedHostComponents(matchedRegistrations, context.getSystemExports(), context.getExtraImports());
140             if (root.elements().size() > 0)
141             {
142                 context.setShouldRequireSpring(true);
143                 context.getFileOverrides().put(SPRING_XML, SpringHelper.documentToBytes(doc));
144             }
145         }
146     }
147 
148     private void addImportsForMatchedHostComponents(List<HostComponentRegistration> matchedRegistrations,
149                                                     SystemExports systemExports, List<String> extraImports)
150     {
151         try
152         {
153             Set<String> referredPackages = OsgiHeaderUtil.findReferredPackageNames(matchedRegistrations);
154 
155             for (String pkg:referredPackages)
156             {
157                 extraImports.add(systemExports.getFullExport(pkg));
158             }
159         }
160         catch (IOException e)
161         {
162             throw new PluginTransformationException("Unable to scan for host component referred packages", e);
163         }
164     }
165 
166 
167     private Set<String> convertRegistrationsToSet(List<HostComponentRegistration> regs)
168     {
169         Set<String> interfaceNames = new HashSet<String>();
170         if (regs != null)
171         {
172             for (HostComponentRegistration reg : regs)
173             {
174                 interfaceNames.addAll(Arrays.asList(reg.getMainInterfaces()));
175             }
176         }
177         return interfaceNames;
178     }
179 
180     private void findUsedHostComponents(Set<String> allHostComponents, Set<String> matchedHostComponents, List<String> innerJarPaths, InputStream
181             jarStream) throws IOException
182     {
183         Set<String> entries = new HashSet<String>();
184         Set<String> superClassNames = new HashSet<String>();
185         ZipInputStream zin = null;
186         try
187         {
188             zin = new ZipInputStream(new BufferedInputStream(jarStream));
189             ZipEntry zipEntry;
190             while ((zipEntry = zin.getNextEntry()) != null)
191             {
192                 String path = zipEntry.getName();
193                 if (path.endsWith(".class"))
194                 {
195                     entries.add(path.substring(0, path.length() - ".class".length()));
196                     final Clazz cls = new Clazz(path, new InputStreamResource(new BufferedInputStream(new UnclosableFilterInputStream(zin))));
197                     final ScanResult scanResult = ClassBinaryScanner.scanClassBinary(cls);
198 
199                     superClassNames.add(scanResult.getSuperClass());
200                     for (String ref : scanResult.getReferredClasses())
201                     {
202                         String name = TransformStageUtils.jarPathToClassName(ref + ".class");
203                         if (allHostComponents.contains(name))
204                         {
205                             matchedHostComponents.add(name);
206                         }
207                     }
208                 }
209                 else if (path.endsWith(".jar") && innerJarPaths.contains(path))
210                 {
211                     findUsedHostComponents(allHostComponents, matchedHostComponents, Collections.<String>emptyList(), new UnclosableFilterInputStream(zin));
212                 }
213             }
214         }
215         finally
216         {
217             IOUtils.closeQuietly(zin);
218         }
219 
220         addHostComponentsUsedInSuperClasses(allHostComponents, matchedHostComponents, entries, superClassNames);
221     }
222 
223     /**
224      * Searches super classes not in the plugin jar, which have methods that use host components
225      * @param allHostComponents The set of all host component classes
226      * @param matchedHostComponents The set of host component classes already found
227      * @param entries The paths of all files in the jar
228      * @param superClassNames All super classes find by classes in the jar
229      */
230     private void addHostComponentsUsedInSuperClasses(Set<String> allHostComponents, Set<String> matchedHostComponents, Set<String> entries, Set<String> superClassNames)
231     {
232         for (String superClassName : superClassNames)
233         {
234             // Only search super classes not in the jar
235             if (!entries.contains(superClassName))
236             {
237                 String cls = superClassName.replace('/','.');
238 
239                 // Ignore java classes including Object
240                 if (!cls.startsWith("java."))
241                 {
242                     Class spr;
243                     try
244                     {
245                         spr = ClassLoaderUtils.loadClass(cls, this.getClass());
246                     }
247                     catch (ClassNotFoundException e)
248                     {
249                         // ignore class not found as it could be from another plugin
250                         continue;
251                     }
252 
253                     // Search methods for parameters that use host components
254                     for (Method m : spr.getMethods())
255                     {
256                         for (Class param : m.getParameterTypes())
257                         {
258                             if (allHostComponents.contains(param.getName()))
259                             {
260                                 matchedHostComponents.add(param.getName());
261                             }
262                         }
263                     }
264                 }
265             }
266         }
267     }
268 
269     private List<String> findJarPaths(Manifest mf)
270     {
271         List<String> paths = new ArrayList<String>();
272         String cp = mf.getMainAttributes().getValue(Constants.BUNDLE_CLASSPATH);
273         if (cp != null)
274         {
275             for (String entry : cp.split(","))
276             {
277                 entry = entry.trim();
278                 if (entry.length() != 1 && entry.endsWith(".jar"))
279                 {
280                     paths.add(entry);
281                 }
282                 else if (!".".equals(entry))
283                 {
284                     log.warn("Non-jar classpath elements not supported: " + entry);
285                 }
286             }
287         }
288         return paths;
289     }
290 
291     /**
292      * Wrapper for the zip input stream to prevent clients from closing it when reading entries
293      */
294     private static class UnclosableFilterInputStream extends FilterInputStream
295     {
296         public UnclosableFilterInputStream(InputStream delegate)
297         {
298             super(delegate);
299         }
300 
301         @Override
302         public void close() throws IOException
303         {
304             // do nothing
305         }
306     }
307 
308     private String determineId(Set<String> hostComponentNames, String beanName, int iteration)
309     {
310         String id = beanName;
311         if (id == null)
312         {
313             id = "bean" + iteration;
314         }
315 
316         id = id.replaceAll("#", "LB");
317 
318         if (hostComponentNames.contains(id))
319         {
320             id += iteration;
321         }
322         return id;
323     }
324 
325     private boolean isRequiredHostComponent(TransformContext context, String name)
326     {
327         for (HostComponentRegistration registration : context.getRequiredHostComponents())
328         {
329             if (Arrays.asList(registration.getMainInterfaces()).contains(name))
330             {
331                 return true;
332             }
333         }
334         return false;
335     }
336 }