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