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