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