1   package com.atlassian.plugins.codegen;
2   
3   import java.io.File;
4   import java.io.FileInputStream;
5   import java.io.FileOutputStream;
6   import java.io.IOException;
7   import java.util.List;
8   import java.util.regex.Matcher;
9   import java.util.regex.Pattern;
10  
11  import com.atlassian.fugue.Option;
12  import com.atlassian.plugins.codegen.AmpsSystemPropertyVariable;
13  import com.atlassian.plugins.codegen.ArtifactDependency;
14  import com.atlassian.plugins.codegen.BundleInstruction;
15  import com.atlassian.plugins.codegen.MavenPlugin;
16  import com.atlassian.plugins.codegen.PluginProjectChangeset;
17  import com.atlassian.plugins.codegen.ProjectRewriter;
18  import com.atlassian.plugins.codegen.VersionId;
19  
20  import com.google.common.base.Predicate;
21  import com.google.common.base.Predicates;
22  import com.google.common.collect.ImmutableList;
23  import com.google.common.collect.ImmutableSet;
24  import com.google.common.collect.Iterables;
25  
26  import org.dom4j.Document;
27  import org.dom4j.DocumentException;
28  import org.dom4j.DocumentHelper;
29  import org.dom4j.Element;
30  import org.dom4j.QName;
31  import org.dom4j.io.OutputFormat;
32  import org.dom4j.io.SAXReader;
33  import org.dom4j.io.XMLWriter;
34  
35  import static com.google.common.base.Preconditions.checkNotNull;
36  import static com.google.common.base.Predicates.and;
37  import static com.google.common.collect.Iterables.any;
38  import static org.apache.commons.io.IOUtils.closeQuietly;
39  
40  /**
41   * Applies any changes from a {@link PluginProjectChangeset} that affect the POM of a Maven project.
42   * These include dependencies, bundle instructions and bundled artifacts in the AMPS configuration,
43   * and arbitrary build plugin configurations.
44   */
45  public class MavenProjectRewriter implements ProjectRewriter
46  {
47      private static final int POM_INDENTATION = 4;
48      
49      private final File pomFile;
50      private final Document document;
51      private final Element root;
52      
53      private static final ImmutableSet<String> AMPS_PLUGIN_IDS =
54          ImmutableSet.of("maven-amps-plugin",
55                          "maven-bamboo-plugin",
56                          "maven-confluence-plugin",
57                          "maven-crowd-plugin",
58                          "maven-fecru-plugin",
59                          "maven-jira-plugin",
60                          "maven-refapp-plugin");
61      
62      public MavenProjectRewriter(File pom) throws DocumentException, IOException
63      {
64          this.pomFile = checkNotNull(pom, "pom");
65          document = readPom(pom);
66          root = document.getRootElement();
67      }
68      
69      @Override
70      public void applyChanges(PluginProjectChangeset changes) throws Exception
71      {
72          boolean modifyPom = false;
73  
74          modifyPom |= applyDependencyChanges(changes.getItems(ArtifactDependency.class));
75          modifyPom |= applyMavenPluginChanges(changes.getItems(MavenPlugin.class));
76          modifyPom |= applyBundleInstructionChanges(changes.getItems(BundleInstruction.class));
77          modifyPom |= applyPluginArtifactChanges(changes.getItems(com.atlassian.plugins.codegen.PluginArtifact.class));
78          modifyPom |= applyAmpsSystemPropertyChanges(changes.getItems(AmpsSystemPropertyVariable.class));
79  
80          if (modifyPom)
81          {
82              writePom(document, pomFile);
83          }
84      }
85  
86      @SuppressWarnings("unchecked")
87      private boolean applyDependencyChanges(Iterable<ArtifactDependency> dependencies)
88      {
89          boolean modified = false;
90          Element eDependencies = getOrCreateElement(root, "dependencies");
91          for (ArtifactDependency descriptor : dependencies)
92          {
93              boolean alreadyExists = any(eDependencies.elements("dependency"),
94                                          and(childElementValue("groupId", descriptor.getGroupAndArtifactId().getGroupId().getOrElse("")),
95                                              childElementValue("artifactId", descriptor.getGroupAndArtifactId().getArtifactId())));
96              if (!alreadyExists)
97              {
98                  modified = true;
99  
100                 Element eNewDep = eDependencies.addElement("dependency");
101                 eNewDep.addElement("groupId").setText(descriptor.getGroupAndArtifactId().getGroupId().get());
102                 eNewDep.addElement("artifactId").setText(descriptor.getGroupAndArtifactId().getArtifactId());
103                 eNewDep.addElement("version").setText(descriptor.getVersionId().getVersionOrPropertyPlaceholder().get());
104                 createVersionPropertyIfNecessary(descriptor.getVersionId());
105                 eNewDep.addElement("scope").setText(descriptor.getScope().name().toLowerCase());
106             }
107         }
108         return modified;
109     }
110 
111     private void createVersionPropertyIfNecessary(VersionId versionId)
112     {
113         for (String p : versionId.getPropertyName())
114         {
115             Element eProperties = getOrCreateElement(root, "properties");
116             if (eProperties.element(p) == null)
117             {
118                 eProperties.addElement(p).setText(versionId.getVersion().getOrElse(""));
119             }
120         }
121     }
122     
123     @SuppressWarnings("unchecked")
124     private boolean applyMavenPluginChanges(Iterable<MavenPlugin> mavenPlugins) throws Exception
125     {
126         boolean modified = false;
127         Element ePlugins = getOrCreateElement(root, "build/plugins");
128         for (MavenPlugin descriptor : mavenPlugins)
129         {
130             Document fragDoc = DocumentHelper.parseText("<root>" + descriptor.getXmlContent() + "</root>");
131             Option<String> groupId = descriptor.getGroupAndArtifactId().getGroupId();
132             String artifactId = descriptor.getGroupAndArtifactId().getArtifactId();
133             Predicate<Element> matchGroup = (Predicate<Element>) (groupId.isDefined() ?
134                 childElementValue("groupId", groupId.get()) :
135                 Predicates.or(childElementValue("groupId", ""), childElementValue("groupId", "org.apache.maven.plugins")));
136             Predicate<Element> match = Predicates.and(matchGroup, childElementValue("artifactId", artifactId));
137             if (Iterables.any(ePlugins.elements("plugin"), match))
138             {
139                 modified |= mergeMavenPluginConfig(Iterables.find((List<Element>) ePlugins.elements("plugin"), match), fragDoc.getRootElement());
140             }
141             else
142             {
143                 ePlugins.add(toMavenPluginElement(descriptor, fragDoc.getRootElement()));
144                 modified = true;
145             }
146         }
147         return modified;
148     }
149     
150     @SuppressWarnings("unchecked")
151     private boolean mergeMavenPluginConfig(Element ePlugin, Element paramsDesc)
152     {
153         boolean modified = false;
154         Element eExecutions = getOrCreateElement(ePlugin, "executions");
155         for (Object node : paramsDesc.selectNodes("executions/execution"))
156         {
157             Element eExecution = (Element) node;
158             String id = eExecution.elementTextTrim("id");
159             if (!Iterables.any(eExecutions.elements("execution"), childElementValue("id", id)))
160             {
161                 detachAndAdd(eExecution, eExecutions);
162                 modified = true;
163             }
164         }
165         return modified;
166     }
167     
168     private Element toMavenPluginElement(MavenPlugin descriptor, Element paramsDesc)
169     {
170         Element p = createElement("plugin");
171         for (String groupId : descriptor.getGroupAndArtifactId().getGroupId())
172         {
173             p.addElement("groupId").setText(groupId);
174         }
175         p.addElement("artifactId").setText(descriptor.getGroupAndArtifactId().getArtifactId());
176         if (descriptor.getVersionId().isDefined())
177         {
178             p.addElement("version").setText(descriptor.getVersionId().getVersionOrPropertyPlaceholder().get());
179             createVersionPropertyIfNecessary(descriptor.getVersionId());
180         }
181         if ("true".equals(paramsDesc.elementText("extensions")))
182         {
183             p.addElement("extensions").setText("true");
184         }
185         for (Object oParam : paramsDesc.elements())
186         {
187             detachAndAdd((Element) oParam, p);
188         }
189         return p;
190     }
191 
192     private boolean applyBundleInstructionChanges(Iterable<BundleInstruction> instructions)
193     {
194         Element configRoot = getAmpsPluginConfiguration();
195         boolean modified = false;
196         Element instructionsRoot = getOrCreateElement(configRoot, "instructions");
197         for (BundleInstruction instruction : instructions)
198         {
199             String categoryName = instruction.getCategory().getElementName();
200             Element categoryElement = getOrCreateElement(instructionsRoot, categoryName);
201             String body = categoryElement.getText();
202             String[] instructionLines = (body == null) ? new String[0] : body.split(",");
203             if (any(ImmutableList.copyOf(instructionLines), bundleInstructionLineWithPackageName(instruction.getPackageName())))
204             {
205                 continue;
206             }
207             categoryElement.setText(addInstructionLine(instructionLines, instruction));
208             modified = true;
209         }
210         return modified;
211     }
212     
213     private static String addInstructionLine(String[] instructionLines, BundleInstruction instruction)
214     {
215         String newLine = instruction.getPackageName();
216         for (String version : instruction.getVersion())
217         {
218             newLine = newLine + ";version=\"" + version + "\"";
219         }
220         if ((instructionLines.length == 0) || instructionLines[0].trim().equals(""))
221         {
222             return newLine;
223         }
224         StringBuilder buf = new StringBuilder();
225         boolean inserted = false;
226         String indent = "";
227         Pattern indentRegex = Pattern.compile("^\\n*([ \\t]*).*");
228         for (String oldLine : instructionLines)
229         {
230             if (buf.length() > 0)
231             {
232                 buf.append(",");
233             }
234             if (!inserted && (oldLine.trim().compareTo(newLine) > 0))
235             {
236                 buf.append("\n").append(indent).append(newLine).append(",\n");
237                 inserted = true;
238             }
239             if (indent.equals(""))
240             {
241                 Matcher m = indentRegex.matcher(oldLine);
242                 if (m.matches())
243                 {
244                     indent = m.group(1);
245                 }
246             }
247             buf.append(oldLine);
248         }
249         if (!inserted)
250         {
251             buf.append(",\n").append(newLine);
252         }
253         return buf.toString();
254     }
255     
256     @SuppressWarnings("unchecked")
257     private boolean applyPluginArtifactChanges(Iterable<com.atlassian.plugins.codegen.PluginArtifact> pluginArtifacts)
258     {
259         Element configRoot = getAmpsPluginConfiguration();
260         boolean modified = false;
261         for (com.atlassian.plugins.codegen.PluginArtifact p : pluginArtifacts)
262         {
263             String elementName = p.getType().getElementName();
264             Element artifactsRoot = getOrCreateElement(configRoot, elementName + "s");
265             if (!any(artifactsRoot.elements(elementName),
266                      and(childElementValue("groupId", p.getGroupAndArtifactId().getGroupId().getOrElse("")),
267                          childElementValue("artifactId", p.getGroupAndArtifactId().getArtifactId()))))
268             {
269                 artifactsRoot.add(toArtifactElement(p));
270                 modified = true;
271             }
272         }
273         return modified;
274     }
275 
276     private boolean applyAmpsSystemPropertyChanges(Iterable<AmpsSystemPropertyVariable> propertyVariables)
277     {
278         Element configRoot = getAmpsPluginConfiguration();
279         boolean modified = false;
280         for (AmpsSystemPropertyVariable propertyVariable : propertyVariables)
281         {
282             Element variablesRoot = getOrCreateElement(configRoot, "systemPropertyVariables");
283             if (variablesRoot.element(propertyVariable.getName()) == null)
284             {
285                 variablesRoot.addElement(propertyVariable.getName()).setText(propertyVariable.getValue());
286                 modified = true;
287             }
288         }
289         return modified;
290     }
291     
292     private Element toArtifactElement(com.atlassian.plugins.codegen.PluginArtifact pluginArtifact)
293     {
294         Element ret = createElement(pluginArtifact.getType().getElementName());
295         for (String groupId : pluginArtifact.getGroupAndArtifactId().getGroupId())
296         {
297             ret.addElement("groupId").setText(groupId);
298         }
299         ret.addElement("artifactId").setText(pluginArtifact.getGroupAndArtifactId().getArtifactId());
300         if (pluginArtifact.getVersionId().isDefined())
301         {
302             ret.addElement("version").setText(pluginArtifact.getVersionId().getVersionOrPropertyPlaceholder().get());
303             createVersionPropertyIfNecessary(pluginArtifact.getVersionId());
304         }
305         return ret;
306     }
307     
308     @SuppressWarnings("unchecked")
309     private Element findAmpsPlugin()
310     {
311         for (Element p : (List<Element>) getOrCreateElement(root, "build/plugins").elements("plugin"))
312         {
313             if (p.elementTextTrim("groupId").equals("com.atlassian.maven.plugins")
314                 && AMPS_PLUGIN_IDS.contains(p.elementTextTrim("artifactId")))
315             {
316                 return p;
317             }
318         }
319         throw new IllegalStateException("Could not find AMPS plugin element in POM");
320     }
321 
322     private Element getAmpsPluginConfiguration()
323     {
324         return getOrCreateElement(findAmpsPlugin(), "configuration");
325     }
326     
327     private static Element getOrCreateElement(Element container, String path)
328     {
329         Element last = container;
330         for (String pathName : path.split("/"))
331         {
332             last = container.element(pathName);
333             if (last == null)
334             {
335                 last = container.addElement(pathName);
336             }
337             container = last;
338         }
339         return last;
340     }
341     
342     private Document readPom(File f) throws DocumentException, IOException
343     {
344         final SAXReader reader = new SAXReader();
345         reader.setMergeAdjacentText(true);
346         reader.setStripWhitespaceText(true);
347         return reader.read(new FileInputStream(f));
348     }
349     
350     private void writePom(Document doc, File f) throws IOException
351     {
352         FileOutputStream fos = new FileOutputStream(f);
353         OutputFormat format = OutputFormat.createPrettyPrint();
354         format.setIndentSize(POM_INDENTATION);
355         XMLWriter writer = new XMLWriter(fos, format);
356         try
357         {
358             writer.write(doc);
359         }
360         finally
361         {
362             closeQuietly(fos);
363         }
364     }
365 
366     private Element createElement(String name)
367     {
368         return DocumentHelper.createElement(new QName(name, root.getNamespace()));
369     }
370     
371     private void fixNamespace(Element e)
372     {
373         e.setQName(new QName(e.getName(), root.getNamespace()));
374         for (Object child : e.elements())
375         {
376             fixNamespace((Element) child);
377         }
378     }
379     
380     private void detachAndAdd(Element e, Element container)
381     {
382         e.detach();
383         fixNamespace(e);
384         container.add(e);
385     }
386     
387     private static Predicate<? super Element> childElementValue(final String name, final String value)
388     {
389         return new Predicate<Element>()
390         {
391             public boolean apply(Element input)
392             {
393                 Element child = input.element(name);
394                 return (child == null) ? value.equals("") : value.equals(child.getText());
395             }
396         };
397     }
398     
399     private static Predicate<String> bundleInstructionLineWithPackageName(final String packageName)
400     {
401         return new Predicate<String>()
402         {
403             public boolean apply(String input)
404             {
405                 String s = input.trim();
406                 return s.equals(packageName) || s.startsWith(packageName + ";");
407             }
408         };
409     }
410 }