View Javadoc

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