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