1   package com.atlassian.plugins.codegen;
2   
3   import java.io.FileOutputStream;
4   import java.io.IOException;
5   import java.io.OutputStreamWriter;
6   import java.util.List;
7   import java.util.Map;
8   
9   import com.atlassian.plugins.codegen.modules.PluginModuleLocation;
10  import com.atlassian.plugins.codegen.util.PluginXmlHelper;
11  
12  import com.google.common.collect.ImmutableList;
13  
14  import org.apache.commons.io.IOUtils;
15  import org.dom4j.Document;
16  import org.dom4j.DocumentException;
17  import org.dom4j.Element;
18  import org.dom4j.Node;
19  import org.dom4j.io.OutputFormat;
20  import org.dom4j.io.XMLWriter;
21  
22  import static com.google.common.collect.Iterables.concat;
23  
24  /**
25   * Applies the subset of changes from a {@link PluginProjectChangeset} that affect the
26   * {@code atlassian-plugin.xml} file.
27   */
28  public class PluginXmlRewriter implements ProjectRewriter
29  {
30      private final PluginXmlHelper xmlHelper;
31      private final Document document;
32      
33      public PluginXmlRewriter(PluginModuleLocation location) throws IOException, DocumentException
34      {
35          this.xmlHelper = new PluginXmlHelper(location);
36          this.document = xmlHelper.getDocument();
37      }
38      
39      @Override
40      public void applyChanges(PluginProjectChangeset changes) throws IOException
41      {
42          boolean modified = false;
43          
44          try
45          {
46              if (changes.hasItems(I18nString.class))
47              {
48                  modified |= addI18nResource(xmlHelper.getDefaultI18nName(), xmlHelper.getDefaultI18nLocation());
49              }
50              
51              for (PluginParameter pluginParam : changes.getItems(PluginParameter.class))
52              {
53                  modified |= addPluginInfoParam(pluginParam.getName(), pluginParam.getValue());
54              }
55              
56              for (ComponentImport componentImport : changes.getItems(ComponentImport.class))
57              {
58                  modified |= addComponentImport(componentImport);
59              }
60              
61              for (ComponentDeclaration component : changes.getItems(ComponentDeclaration.class))
62              {
63                  modified |= addComponentDeclaration(component);
64              }
65              
66              for (ModuleDescriptor moduleDescriptor : changes.getItems(ModuleDescriptor.class))
67              {
68                  modified |= addModuleAsLastChild(moduleDescriptor.getContent());
69              }
70          }
71          catch (DocumentException e)
72          {
73              throw new IOException(e);
74          }
75  
76          if (modified)
77          {
78              OutputFormat format = OutputFormat.createPrettyPrint();
79              FileOutputStream fos = new FileOutputStream(xmlHelper.getXmlFile());
80              OutputStreamWriter osw = new OutputStreamWriter(fos);
81              XMLWriter writer = new XMLWriter(osw, format);
82              try
83              {
84                  writer.write(document);
85              }
86              finally
87              {
88                  writer.close();
89                  IOUtils.closeQuietly(osw);
90                  IOUtils.closeQuietly(fos);
91              }
92          }
93      }
94      
95      @SuppressWarnings("unchecked")
96      private Element createModule(String type)
97      {
98          Element newElement = document.getRootElement().addElement(type);
99          List<Element> existingModules = (List<Element>) document.getRootElement().elements(type);
100         if (!existingModules.isEmpty())
101         {
102             newElement.detach();
103             existingModules.add(newElement);
104         }
105         return newElement;
106     }
107     
108     private boolean addModuleAsLastChild(Element module)
109     {
110         String key = module.attributeValue("key");
111         if ((key == null) || !PluginXmlHelper.findElementByTypeAndAttribute(document.getRootElement(), module.getName(), "key", key).isDefined())
112         {
113             Element pluginRoot = document.getRootElement();
114             pluginRoot.add(module);
115             return true;
116         }
117         return false;
118     }
119 
120     private boolean addI18nResource(String name, String location) throws DocumentException, IOException
121     {
122         String xpath = "//resource[@type='i18n' and (@name = '" + name + "' or @location='" + location + "')]";
123         Node resourceNode = document.selectSingleNode(xpath);
124 
125         if (resourceNode == null)
126         {
127             Element resource = document.getRootElement().addElement("resource");
128             resource.addAttribute("type", "i18n");
129             resource.addAttribute("name", name);
130             resource.addAttribute("location", location);
131             return true;
132         }
133         
134         return false;
135     }
136 
137     private boolean addPluginInfoParam(String name, String value)
138     {
139         Element pluginInfo = (Element) document.selectSingleNode("//plugin-info");
140         if (pluginInfo == null)
141         {
142             pluginInfo = document.addElement("plugin-info");
143         }
144         if (!PluginXmlHelper.findElementByTypeAndAttribute(pluginInfo, "param", "name", name).isDefined())
145         {
146             pluginInfo.addElement("param").addAttribute("name", name).setText(value);
147             return true;
148         }
149         return false;
150     }
151     
152     private boolean addComponentImport(ComponentImport componentImport) throws DocumentException
153     {
154         String key = componentImport.getKey().getOrElse(createKeyFromClass(componentImport.getInterfaceClass()));
155         for (ClassId interfaceId : concat(ImmutableList.of(componentImport.getInterfaceClass()), componentImport.getAlternateInterfaces()))
156         {
157             if ((document.getRootElement().selectNodes("//component-import[@interface='" + interfaceId.getFullName() + "']").size() > 0)
158                 || (document.getRootElement().selectNodes("//component-import/interface[text()='" + interfaceId.getFullName() + "']").size() > 0))
159             {
160                 return false;
161             }
162         }
163         if (PluginXmlHelper.findElementByTypeAndAttribute(document.getRootElement(), "component-import", "key", key).isDefined())
164         {
165             return false;
166         }
167         
168         Element element = createModule("component-import");
169         element.addAttribute("key", key);
170         element.addAttribute("interface", componentImport.getInterfaceClass().getFullName());
171         for (String filter : componentImport.getFilter())
172         {
173             element.addAttribute("filter", filter);
174         }
175         return true;
176     }
177     
178     private boolean addComponentDeclaration(ComponentDeclaration component) throws DocumentException
179     {
180         if (!PluginXmlHelper.findElementByTypeAndAttribute(document.getRootElement(), "component", "key", component.getKey()).isDefined())
181         {
182             Element element = createModule("component");
183             element.addAttribute("key", component.getKey());
184             element.addAttribute("class", component.getClassId().getFullName());
185             for (String name : component.getName())
186             {
187                 element.addAttribute("name", name);
188             }
189             for (String nameI18nKey : component.getNameI18nKey())
190             {
191                 element.addAttribute("i18n-name-key", nameI18nKey);
192             }
193             if (component.getVisibility() == ComponentDeclaration.Visibility.PUBLIC)
194             {
195                 element.addAttribute("public", "true");
196             }
197             for (String alias : component.getAlias())
198             {
199                 element.addAttribute("alias", alias);
200             }
201             for (String description : component.getDescription())
202             {
203                 Element eDesc = element.addElement("description");
204                 eDesc.setText(description);
205                 for (String descI18nKey : component.getDescriptionI18nKey())
206                 {
207                     eDesc.addAttribute("key", descI18nKey);
208                 }
209             }
210             for (ClassId interfaceId : component.getInterfaceId())
211             {
212                 element.addElement("interface").setText(interfaceId.getFullName());
213             }
214             if (!component.getServiceProperties().isEmpty())
215             {
216                 Element eProps = element.addElement("service-properties");
217                 for (Map.Entry<String, String> entry : component.getServiceProperties().entrySet())
218                 {
219                     Element eEntry = eProps.addElement("entry");
220                     eEntry.addAttribute("key", entry.getKey());
221                     eEntry.addAttribute("value", entry.getValue());
222                 }
223             }
224             return true;
225         }
226         return false;
227     }
228     
229     private String createKeyFromClass(ClassId classId)
230     {
231         return lowercaseFirst(classId.getName());
232     }
233     
234     private String lowercaseFirst(String input)
235     {
236         return input.equals("") ? input : (input.substring(0, 1).toLowerCase() + input.substring(1));
237     }   
238 }