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.apache.commons.lang.StringUtils;
27  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
28  import org.dom4j.Document;
29  import org.dom4j.DocumentException;
30  import org.dom4j.DocumentHelper;
31  import org.dom4j.Element;
32  import org.dom4j.QName;
33  import org.dom4j.io.OutputFormat;
34  import org.dom4j.io.SAXReader;
35  import org.dom4j.io.XMLWriter;
36  
37  import static com.google.common.base.Preconditions.checkNotNull;
38  import static com.google.common.base.Predicates.and;
39  import static com.google.common.collect.Iterables.any;
40  import static org.apache.commons.io.IOUtils.closeQuietly;
41  
42  /**
43   * Applies any changes from a {@link PluginProjectChangeset} that affect the POM of a Maven project.
44   * These include dependencies, bundle instructions and bundled artifacts in the AMPS configuration,
45   * and arbitrary build plugin configurations.
46   */
47  public class MavenProjectRewriter implements ProjectRewriter
48  {
49      private static final int POM_INDENTATION = 4;
50      
51      private final File pomFile;
52      private final Document document;
53      private final Element root;
54      
55      private static final ImmutableSet<String> AMPS_PLUGIN_IDS =
56          ImmutableSet.of("maven-amps-plugin",
57                          "maven-bamboo-plugin",
58                          "maven-confluence-plugin",
59                          "maven-crowd-plugin",
60                          "maven-fecru-plugin",
61                          "maven-stash-plugin",
62                          "maven-jira-plugin",
63                          "maven-refapp-plugin");
64      
65      public MavenProjectRewriter(File pom) throws DocumentException, IOException
66      {
67          this.pomFile = checkNotNull(pom, "pom");
68          document = readPom(pom);
69          root = document.getRootElement();
70      }
71      
72      @Override
73      public void applyChanges(PluginProjectChangeset changes) throws Exception
74      {
75          boolean modifyPom = false;
76  
77          modifyPom |= applyDependencyChanges(changes.getItems(ArtifactDependency.class));
78          modifyPom |= applyMavenPluginChanges(changes.getItems(MavenPlugin.class));
79          modifyPom |= applyBundleInstructionChanges(changes.getItems(BundleInstruction.class));
80          modifyPom |= applyPluginArtifactChanges(changes.getItems(com.atlassian.plugins.codegen.PluginArtifact.class));
81          modifyPom |= applyAmpsSystemPropertyChanges(changes.getItems(AmpsSystemPropertyVariable.class));
82          modifyPom |= applyAmpsVersionUpdate(changes.getItems(AmpsVersionUpdate.class));
83  
84          if (modifyPom)
85          {
86              writePom(document, pomFile);
87          }
88      }
89  
90      @SuppressWarnings("unchecked")
91      private boolean applyDependencyChanges(Iterable<ArtifactDependency> dependencies)
92      {
93          boolean modified = false;
94          Element eDependencies = getOrCreateElement(root, "dependencies");
95          for (ArtifactDependency descriptor : dependencies)
96          {
97              boolean alreadyExists = any(eDependencies.elements("dependency"),
98                                          and(childElementValue("groupId", descriptor.getGroupAndArtifactId().getGroupId().getOrElse("")),
99                                              childElementValue("artifactId", descriptor.getGroupAndArtifactId().getArtifactId())));
100             if (!alreadyExists)
101             {
102                 modified = true;
103 
104                 Element eNewDep = eDependencies.addElement("dependency");
105                 eNewDep.addElement("groupId").setText(descriptor.getGroupAndArtifactId().getGroupId().get());
106                 eNewDep.addElement("artifactId").setText(descriptor.getGroupAndArtifactId().getArtifactId());
107                 eNewDep.addElement("version").setText(descriptor.getVersionId().getVersionOrPropertyPlaceholder().get());
108                 createVersionPropertyIfNecessary(descriptor.getVersionId());
109                 eNewDep.addElement("scope").setText(descriptor.getScope().name().toLowerCase());
110             }
111         }
112         return modified;
113     }
114 
115     private void createVersionPropertyIfNecessary(VersionId versionId)
116     {
117         for (String p : versionId.getPropertyName())
118         {
119             Element eProperties = getOrCreateElement(root, "properties");
120             if (eProperties.element(p) == null)
121             {
122                 eProperties.addElement(p).setText(versionId.getVersion().getOrElse(""));
123             }
124         }
125     }
126     
127     @SuppressWarnings("unchecked")
128     private boolean applyMavenPluginChanges(Iterable<MavenPlugin> mavenPlugins) throws Exception
129     {
130         boolean modified = false;
131         Element ePlugins = getOrCreateElement(root, "build/plugins");
132         for (MavenPlugin descriptor : mavenPlugins)
133         {
134             Document fragDoc = DocumentHelper.parseText("<root>" + descriptor.getXmlContent() + "</root>");
135             Option<String> groupId = descriptor.getGroupAndArtifactId().getGroupId();
136             String artifactId = descriptor.getGroupAndArtifactId().getArtifactId();
137             Predicate<Element> matchGroup = (Predicate<Element>) (groupId.isDefined() ?
138                 childElementValue("groupId", groupId.get()) :
139                 Predicates.or(childElementValue("groupId", ""), childElementValue("groupId", "org.apache.maven.plugins")));
140             Predicate<Element> match = Predicates.and(matchGroup, childElementValue("artifactId", artifactId));
141             if (Iterables.any(ePlugins.elements("plugin"), match))
142             {
143                 modified |= mergeMavenPluginConfig(Iterables.find((List<Element>) ePlugins.elements("plugin"), match), fragDoc.getRootElement());
144             }
145             else
146             {
147                 ePlugins.add(toMavenPluginElement(descriptor, fragDoc.getRootElement()));
148                 modified = true;
149             }
150         }
151         return modified;
152     }
153 
154     @SuppressWarnings("unchecked")
155     private boolean applyAmpsVersionUpdate(Iterable<AmpsVersionUpdate> items)
156     {
157         boolean modified = false;
158         
159         //find the highest version in our items.
160         //Note: really there should only be 1 change item
161         DefaultArtifactVersion newAmpsVersion = new DefaultArtifactVersion("0.0");
162         for(AmpsVersionUpdate changeItem : items)
163         {
164             DefaultArtifactVersion changeVersion = new DefaultArtifactVersion(changeItem.getVersion());
165             if(changeVersion.compareTo(newAmpsVersion) > 0)
166             {
167                 newAmpsVersion = changeVersion;
168             }
169             
170             if(AmpsVersionUpdate.PLUGIN.equalsIgnoreCase(changeItem.getType()) && changeItem.isApplyConfig())
171             {
172                 modified = applyAmpsPluginVersionUpdate();
173             }
174 
175             if(AmpsVersionUpdate.MANAGEMENT.equalsIgnoreCase(changeItem.getType()) && changeItem.isApplyConfig())
176             {
177                 boolean managementUpdated = applyAmpsPluginManagementVersionUpdate();
178                 if(!modified)
179                 {
180                     modified = managementUpdated;
181                 }
182             }
183             
184             if(changeItem.isApplyProp())
185             {
186                 //add the amps.version prop if needed
187                 Element ampsVersionProperty = getOrCreateElement(getOrCreateElement(root, "properties"),"amps.version");
188 
189                 //update the amps.version prop if our change is a newer version
190                 if(StringUtils.isNotBlank(ampsVersionProperty.getTextTrim()))
191                 {
192                     DefaultArtifactVersion pomVersion = new DefaultArtifactVersion(ampsVersionProperty.getTextTrim());
193                     if(newAmpsVersion.compareTo(pomVersion) > 0)
194                     {
195                         modified = true;
196                         ampsVersionProperty.setText(newAmpsVersion.toString());
197                     }
198                 }
199                 else
200                 {
201                     ampsVersionProperty.setText(newAmpsVersion.toString());
202                     modified = true;
203                 }
204             }
205         }
206 
207         return modified;
208     }
209     
210     private boolean applyAmpsPluginVersionUpdate()
211     {
212         boolean modified = false;
213         
214         //update the amps plugin version to the property if needed
215         Element ampsVersionElement = getOrCreateElement(findAmpsPlugin(),"version");
216         if(!"${amps.version}".equals(ampsVersionElement.getTextTrim()))
217         {
218             ampsVersionElement.setText("${amps.version}");
219             modified = true;
220         }
221         
222         return modified;
223     }
224 
225     private boolean applyAmpsPluginManagementVersionUpdate()
226     {
227         boolean modified = false;
228         //update the amps plugin version to the property if needed
229         Element ampsManagementPlugin = findAmpsPluginManagement();
230         if(null != ampsManagementPlugin)
231         {
232             Element ampsVersionElement = getOrCreateElement(ampsManagementPlugin,"version");
233             if(!"${amps.version}".equals(ampsVersionElement.getTextTrim()))
234             {
235                 ampsVersionElement.setText("${amps.version}");
236                 modified = true;
237             }
238         }
239         
240         return modified;
241     }
242     
243     public String getAmpsVersionInPom()
244     {
245         Element ampsVersion = getElementOrNull(findAmpsPlugin(),"version");
246         if(null != ampsVersion)
247         {
248             return ampsVersion.getTextTrim();
249         }
250         
251         return "";
252     }
253 
254     public boolean definesProperty(String propName)
255     {
256         Element properties = getElementOrNull(root, "properties");
257         if(null != properties)
258         {
259             return null != getElementOrNull(properties,propName);
260         }
261 
262         return false;
263     }
264 
265     public String getAmpsPluginManagementVersionInPom()
266     {
267         Element ampsManagementPlugin = findAmpsPluginManagement();
268         String version = "";
269         if(null != ampsManagementPlugin)
270         {
271             Element ampsVersion = getElementOrNull(ampsManagementPlugin,"version");
272             if(null != ampsVersion)
273             {
274                 version = ampsVersion.getTextTrim();
275             }
276         }
277         
278         return version;
279     }
280     
281     @SuppressWarnings("unchecked")
282     private boolean mergeMavenPluginConfig(Element ePlugin, Element paramsDesc)
283     {
284         boolean modified = false;
285         Element eExecutions = getOrCreateElement(ePlugin, "executions");
286         for (Object node : paramsDesc.selectNodes("executions/execution"))
287         {
288             Element eExecution = (Element) node;
289             String id = eExecution.elementTextTrim("id");
290             if (!Iterables.any(eExecutions.elements("execution"), childElementValue("id", id)))
291             {
292                 detachAndAdd(eExecution, eExecutions);
293                 modified = true;
294             }
295         }
296         return modified;
297     }
298     
299     private Element toMavenPluginElement(MavenPlugin descriptor, Element paramsDesc)
300     {
301         Element p = createElement("plugin");
302         for (String groupId : descriptor.getGroupAndArtifactId().getGroupId())
303         {
304             p.addElement("groupId").setText(groupId);
305         }
306         p.addElement("artifactId").setText(descriptor.getGroupAndArtifactId().getArtifactId());
307         if (descriptor.getVersionId().isDefined())
308         {
309             p.addElement("version").setText(descriptor.getVersionId().getVersionOrPropertyPlaceholder().get());
310             createVersionPropertyIfNecessary(descriptor.getVersionId());
311         }
312         if ("true".equals(paramsDesc.elementText("extensions")))
313         {
314             p.addElement("extensions").setText("true");
315         }
316         for (Object oParam : paramsDesc.elements())
317         {
318             detachAndAdd((Element) oParam, p);
319         }
320         return p;
321     }
322 
323     private boolean applyBundleInstructionChanges(Iterable<BundleInstruction> instructions)
324     {
325         if(!instructions.iterator().hasNext())
326         {
327             return false;
328         }
329         
330         Element configRoot = getAmpsPluginConfiguration();
331         boolean modified = false;
332         Element instructionsRoot = getOrCreateElement(configRoot, "instructions");
333         for (BundleInstruction instruction : instructions)
334         {
335             String categoryName = instruction.getCategory().getElementName();
336             Element categoryElement = getOrCreateElement(instructionsRoot, categoryName);
337             String body = categoryElement.getText();
338             String[] instructionLines = (body == null) ? new String[0] : body.split(",");
339             if (any(ImmutableList.copyOf(instructionLines), bundleInstructionLineWithPackageName(instruction.getPackageName())))
340             {
341                 continue;
342             }
343             categoryElement.setText(addInstructionLine(instructionLines, instruction));
344             modified = true;
345         }
346         return modified;
347     }
348     
349     private static String addInstructionLine(String[] instructionLines, BundleInstruction instruction)
350     {
351         String newLine = instruction.getPackageName();
352         for (String version : instruction.getVersion())
353         {
354             newLine = newLine + ";version=\"" + version + "\"";
355         }
356         if ((instructionLines.length == 0) || instructionLines[0].trim().equals(""))
357         {
358             return newLine;
359         }
360         StringBuilder buf = new StringBuilder();
361         boolean inserted = false;
362         String indent = "";
363         Pattern indentRegex = Pattern.compile("^\\n*([ \\t]*).*");
364         for (String oldLine : instructionLines)
365         {
366             if (buf.length() > 0)
367             {
368                 buf.append(",");
369             }
370             if (!inserted && (oldLine.trim().compareTo(newLine) > 0))
371             {
372                 buf.append("\n").append(indent).append(newLine).append(",\n");
373                 inserted = true;
374             }
375             if (indent.equals(""))
376             {
377                 Matcher m = indentRegex.matcher(oldLine);
378                 if (m.matches())
379                 {
380                     indent = m.group(1);
381                 }
382             }
383             buf.append(oldLine);
384         }
385         if (!inserted)
386         {
387             buf.append(",\n").append(newLine);
388         }
389         return buf.toString();
390     }
391     
392     @SuppressWarnings("unchecked")
393     private boolean applyPluginArtifactChanges(Iterable<com.atlassian.plugins.codegen.PluginArtifact> pluginArtifacts)
394     {
395         if(!pluginArtifacts.iterator().hasNext())
396         {
397             return false;
398         }
399         
400         Element configRoot = getAmpsPluginConfiguration();
401         boolean modified = false;
402         for (com.atlassian.plugins.codegen.PluginArtifact p : pluginArtifacts)
403         {
404             String elementName = p.getType().getElementName();
405             Element artifactsRoot = getOrCreateElement(configRoot, elementName + "s");
406             if (!any(artifactsRoot.elements(elementName),
407                      and(childElementValue("groupId", p.getGroupAndArtifactId().getGroupId().getOrElse("")),
408                          childElementValue("artifactId", p.getGroupAndArtifactId().getArtifactId()))))
409             {
410                 artifactsRoot.add(toArtifactElement(p));
411                 modified = true;
412             }
413         }
414         return modified;
415     }
416 
417     private boolean applyAmpsSystemPropertyChanges(Iterable<AmpsSystemPropertyVariable> propertyVariables)
418     {
419         if(!propertyVariables.iterator().hasNext())
420         {
421             return false;
422         }
423         
424         Element configRoot = getAmpsPluginConfiguration();
425         boolean modified = false;
426         for (AmpsSystemPropertyVariable propertyVariable : propertyVariables)
427         {
428             Element variablesRoot = getOrCreateElement(configRoot, "systemPropertyVariables");
429             if (variablesRoot.element(propertyVariable.getName()) == null)
430             {
431                 variablesRoot.addElement(propertyVariable.getName()).setText(propertyVariable.getValue());
432                 modified = true;
433             }
434         }
435         return modified;
436     }
437     
438     private Element toArtifactElement(com.atlassian.plugins.codegen.PluginArtifact pluginArtifact)
439     {
440         Element ret = createElement(pluginArtifact.getType().getElementName());
441         for (String groupId : pluginArtifact.getGroupAndArtifactId().getGroupId())
442         {
443             ret.addElement("groupId").setText(groupId);
444         }
445         ret.addElement("artifactId").setText(pluginArtifact.getGroupAndArtifactId().getArtifactId());
446         if (pluginArtifact.getVersionId().isDefined())
447         {
448             ret.addElement("version").setText(pluginArtifact.getVersionId().getVersionOrPropertyPlaceholder().get());
449             createVersionPropertyIfNecessary(pluginArtifact.getVersionId());
450         }
451         return ret;
452     }
453     
454     @SuppressWarnings("unchecked")
455     private Element findAmpsPlugin()
456     {
457         Element plugins = getElementOrNull(root, "build/plugins");
458         if(null != plugins)
459         {
460             for (Element p : (List<Element>) plugins.elements("plugin"))
461             {
462                 if (p.elementTextTrim("groupId").equals("com.atlassian.maven.plugins")
463                     && AMPS_PLUGIN_IDS.contains(p.elementTextTrim("artifactId")))
464                 {
465                     return p;
466                 }
467             }
468         }
469         throw new IllegalStateException("Could not find AMPS plugin element in POM");
470     }
471 
472     @SuppressWarnings("unchecked")
473     private Element findAmpsPluginManagement()
474     {
475         Element plugins = getElementOrNull(root, "build/pluginManagement/plugins");
476         if(null != plugins)
477         {
478             for (Element p : (List<Element>) plugins.elements("plugin"))
479             {
480                 if (p.elementTextTrim("groupId").equals("com.atlassian.maven.plugins")
481                         && AMPS_PLUGIN_IDS.contains(p.elementTextTrim("artifactId")))
482                 {
483                     return p;
484                 }
485             }
486         }
487         
488         return null;
489     }
490 
491     private Element getAmpsPluginConfiguration()
492     {
493         return getOrCreateElement(findAmpsPlugin(), "configuration");
494     }
495     
496     private static Element getOrCreateElement(Element container, String path)
497     {
498         Element last = container;
499         for (String pathName : path.split("/"))
500         {
501             last = container.element(pathName);
502             if (last == null)
503             {
504                 last = container.addElement(pathName);
505             }
506             container = last;
507         }
508         return last;
509     }
510 
511     private static Element getElementOrNull(Element container, String path)
512     {
513         for (String pathName : path.split("/"))
514         {
515             if (container != null)
516             {
517                 container = container.element(pathName);
518             }
519         }
520         return container;
521     }
522     
523     private Document readPom(File f) throws DocumentException, IOException
524     {
525         final SAXReader reader = new SAXReader();
526         reader.setMergeAdjacentText(true);
527         reader.setStripWhitespaceText(true);
528         return reader.read(new FileInputStream(f));
529     }
530     
531     private void writePom(Document doc, File f) throws IOException
532     {
533         FileOutputStream fos = new FileOutputStream(f);
534         OutputFormat format = OutputFormat.createPrettyPrint();
535         format.setIndentSize(POM_INDENTATION);
536         XMLWriter writer = new XMLWriter(fos, format);
537         try
538         {
539             writer.write(doc);
540         }
541         finally
542         {
543             closeQuietly(fos);
544         }
545     }
546 
547     private Element createElement(String name)
548     {
549         return DocumentHelper.createElement(new QName(name, root.getNamespace()));
550     }
551     
552     private void fixNamespace(Element e)
553     {
554         e.setQName(new QName(e.getName(), root.getNamespace()));
555         for (Object child : e.elements())
556         {
557             fixNamespace((Element) child);
558         }
559     }
560     
561     private void detachAndAdd(Element e, Element container)
562     {
563         e.detach();
564         fixNamespace(e);
565         container.add(e);
566     }
567     
568     private static Predicate<? super Element> childElementValue(final String name, final String value)
569     {
570         return new Predicate<Element>()
571         {
572             public boolean apply(Element input)
573             {
574                 Element child = input.element(name);
575                 return (child == null) ? value.equals("") : value.equals(child.getText());
576             }
577         };
578     }
579     
580     private static Predicate<String> bundleInstructionLineWithPackageName(final String packageName)
581     {
582         return new Predicate<String>()
583         {
584             public boolean apply(String input)
585             {
586                 String s = input.trim();
587                 return s.equals(packageName) || s.startsWith(packageName + ";");
588             }
589         };
590     }
591 }