View Javadoc
1   package com.atlassian.plugin.osgi.factory.transform.stage;
2   
3   import com.atlassian.plugin.osgi.factory.transform.PluginTransformationException;
4   import com.atlassian.plugin.osgi.factory.transform.TransformContext;
5   import com.atlassian.plugin.osgi.factory.transform.TransformStage;
6   import com.atlassian.plugin.util.PluginUtils;
7   import com.atlassian.plugin.util.validation.ValidationPattern;
8   import com.google.common.collect.Sets;
9   import org.apache.commons.lang3.StringUtils;
10  import org.dom4j.Document;
11  import org.dom4j.Element;
12  import org.slf4j.Logger;
13  import org.slf4j.LoggerFactory;
14  
15  import java.io.File;
16  import java.io.FileInputStream;
17  import java.io.IOException;
18  import java.util.ArrayList;
19  import java.util.Collections;
20  import java.util.LinkedHashSet;
21  import java.util.List;
22  import java.util.Set;
23  import java.util.jar.JarInputStream;
24  
25  import static com.atlassian.plugin.util.validation.ValidationPattern.createPattern;
26  import static com.atlassian.plugin.util.validation.ValidationPattern.test;
27  
28  /**
29   * Transforms component tags in the plugin descriptor into the appropriate spring XML configuration file
30   *
31   * @since 2.2.0
32   */
33  public class ComponentSpringStage implements TransformStage {
34      /**
35       * Path of generated Spring XML file
36       */
37      private static final String SPRING_XML = "META-INF/spring/atlassian-plugins-components.xml";
38  
39      public static final String BEAN_SOURCE = "Plugin Component";
40  
41      private static final Logger LOG = LoggerFactory.getLogger(ComponentSpringStage.class);
42  
43      public void execute(TransformContext context) throws PluginTransformationException {
44          if (SpringHelper.shouldGenerateFile(context, SPRING_XML)) {
45              Document springDoc = SpringHelper.createSpringDocument();
46              Element root = springDoc.getRootElement();
47              List<Element> elements = context.getDescriptorDocument().getRootElement().elements("component");
48  
49              ValidationPattern validation = createPattern().
50                      rule(
51                              test("@key").withError("The key is required"),
52                              test("@class").withError("The class is required"),
53                              test("not(@public='true') or interface or @interface").withError("Interfaces must be declared for public components"),
54                              test("not(service-properties) or count(service-properties/entry[@key and @value]) > 0")
55                                      .withError("The service-properties element must contain at least one entry element with key and value attributes"));
56  
57              final Set<String> declaredInterfaces = new LinkedHashSet<>();
58  
59              for (Element component : elements) {
60                  if (!PluginUtils.doesModuleElementApplyToApplication(component, context.getApplications(), context.getInstallationMode())) {
61                      continue;
62                  }
63                  validation.evaluate(component);
64  
65  
66                  String beanId = component.attributeValue("key");
67                  // make sure the new bean id is not already in use.
68                  context.trackBean(beanId, BEAN_SOURCE);
69  
70                  Element bean = root.addElement("beans:bean");
71                  bean.addAttribute("id", beanId);
72                  bean.addAttribute("autowire", "default");
73  
74                  // alias attribute in atlassian-plugin gets converted into alias element.
75                  if (!StringUtils.isBlank(component.attributeValue("alias"))) {
76                      Element alias = root.addElement("beans:alias");
77                      alias.addAttribute("name", beanId);
78                      alias.addAttribute("alias", component.attributeValue("alias"));
79                  }
80  
81                  List<String> interfaceNames = new ArrayList<>();
82                  List<Element> compInterfaces = component.elements("interface");
83                  for (Element inf : compInterfaces) {
84                      interfaceNames.add(inf.getTextTrim());
85                  }
86                  if (component.attributeValue("interface") != null) {
87                      interfaceNames.add(component.attributeValue("interface"));
88                  }
89  
90                  bean.addAttribute("class", component.attributeValue("class"));
91  
92                  if ("true".equalsIgnoreCase(component.attributeValue("public"))) {
93                      Element osgiService = root.addElement("osgi:service");
94                      osgiService.addAttribute("id", component.attributeValue("key") + "_osgiService");
95                      osgiService.addAttribute("ref", component.attributeValue("key"));
96  
97  
98                      // Collect for the interface names which will be used for import generation.
99                      declaredInterfaces.addAll(interfaceNames);
100 
101                     Element interfaces = osgiService.addElement("osgi:interfaces");
102                     for (String name : interfaceNames) {
103                         ensureExported(name, context);
104                         Element e = interfaces.addElement("beans:value");
105                         e.setText(name);
106                     }
107 
108                     Element svcprops = component.element("service-properties");
109                     if (svcprops != null) {
110                         Element targetSvcprops = osgiService.addElement("osgi:service-properties");
111                         for (Element prop : new ArrayList<Element>(svcprops.elements("entry"))) {
112                             Element e = targetSvcprops.addElement("beans:entry");
113                             e.addAttribute("key", prop.attributeValue("key"));
114                             e.addAttribute("value", prop.attributeValue("value"));
115                         }
116                     }
117                 }
118             }
119 
120             if (root.elements().size() > 0) {
121                 context.setShouldRequireSpring(true);
122                 context.getFileOverrides().put(SPRING_XML, SpringHelper.documentToBytes(springDoc));
123             }
124 
125             // calculate the required interfaces to be imported. this is (all the classes) - (classes available in the plugin).
126             Set<String> requiredInterfaces;
127             try {
128                 requiredInterfaces = calculateRequiredImports(context.getPluginFile(),
129                         declaredInterfaces,
130                         context.getBundleClassPathJars());
131             } catch (PluginTransformationException e) {
132                 throw new PluginTransformationException("Error while calculating import manifest", e);
133             }
134 
135             // dump all the outstanding imports as extra imports.
136             context.getExtraImports().addAll(TransformStageUtils.getPackageNames(requiredInterfaces));
137         }
138     }
139 
140     private void ensureExported(String className, TransformContext context) {
141         String pkg = className.substring(0, className.lastIndexOf('.'));
142         if (!context.getExtraExports().contains(pkg)) {
143             String fileName = className.replace('.', '/') + ".class";
144 
145             if (context.getPluginArtifact().doesResourceExist(fileName)) {
146                 context.getExtraExports().add(pkg);
147             }
148         }
149     }
150 
151     /**
152      * Calculate the the interfaces that need to be imported.
153      *
154      * @return the set of interfaces that cannot be resolved in the pluginFile.
155      */
156     private Set<String> calculateRequiredImports(final File pluginFile,
157                                                  final Set<String> declaredInterfaces,
158                                                  final Set<String> innerJars) {
159         // we only do it if at least one interface is declared as part of component element.
160         if (declaredInterfaces.size() > 0) {
161             // scan for class files of interest in the jar file, not including classes in inner jars.
162             final Set<String> shallowMatches;
163             FileInputStream fis = null;
164             JarInputStream jarStream = null;
165             try {
166                 fis = new FileInputStream(pluginFile);
167                 jarStream = new JarInputStream(fis);
168                 shallowMatches = TransformStageUtils.scanJarForItems(jarStream,
169                         declaredInterfaces,
170                         TransformStageUtils.JarEntryToClassName.INSTANCE);
171             } catch (final IOException ioe) {
172                 throw new PluginTransformationException("Error reading jar:" + pluginFile.getName(), ioe);
173             } finally {
174                 TransformStageUtils.closeNestedStreamQuietly(jarStream, fis);
175             }
176 
177             // the outstanding set = declared set - shallow match set
178             final Set<String> remainders = Sets.newLinkedHashSet(Sets.difference(declaredInterfaces, shallowMatches));
179 
180             // if all the interfaces are not yet satisfied, we have to scan inner jars as well.
181             // this is, of course, subject to the availability of qualified inner jars.
182             if ((remainders.size() > 0) && (innerJars.size() > 0)) {
183                 remainders.removeAll(TransformStageUtils.scanInnerJars(pluginFile, innerJars, remainders));
184             }
185 
186             return Collections.unmodifiableSet(remainders);
187         }
188 
189         // if no need to import.
190         return Collections.emptySet();
191     }
192 }