1   package com.atlassian.maven.plugins.amps;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.io.StringReader;
6   import java.util.List;
7   
8   import com.atlassian.plugins.codegen.AmpsSystemPropertyVariable;
9   import com.atlassian.plugins.codegen.ArtifactDependency;
10  import com.atlassian.plugins.codegen.ArtifactId;
11  import com.atlassian.plugins.codegen.BundleInstruction;
12  import com.atlassian.plugins.codegen.MavenPlugin;
13  import com.atlassian.plugins.codegen.PluginProjectChangeset;
14  import com.atlassian.plugins.codegen.ProjectRewriter;
15  import com.atlassian.plugins.codegen.VersionId;
16  
17  import com.google.common.base.Predicate;
18  import com.google.common.collect.ImmutableList;
19  import com.google.common.collect.ImmutableSet;
20  import com.google.common.collect.Iterables;
21  import com.google.common.collect.Ordering;
22  
23  import org.apache.commons.io.IOUtils;
24  import org.apache.commons.io.output.XmlStreamWriter;
25  import org.apache.maven.model.Dependency;
26  import org.apache.maven.model.Model;
27  import org.apache.maven.model.Plugin;
28  import org.apache.maven.model.PluginExecution;
29  import org.apache.maven.plugin.logging.Log;
30  import org.apache.maven.plugin.logging.SystemStreamLog;
31  import org.apache.maven.plugins.shade.pom.PomWriter;
32  import org.apache.maven.project.MavenProject;
33  import org.codehaus.plexus.util.xml.Xpp3Dom;
34  import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
35  import org.dom4j.Document;
36  import org.dom4j.DocumentHelper;
37  import org.dom4j.Element;
38  import org.dom4j.Node;
39  
40  import static com.atlassian.fugue.Option.none;
41  import static com.atlassian.fugue.Option.option;
42  import static com.atlassian.fugue.Option.some;
43  import static com.google.common.base.Preconditions.checkNotNull;
44  import static com.google.common.collect.Iterables.any;
45  import static com.google.common.collect.Iterables.concat;
46  
47  /**
48   * Applies any changes from a {@link PluginProjectChangeset} that affect the POM of a Maven project.
49   * These include dependencies, bundle instructions and bundled artifacts in the AMPS configuration,
50   * and arbitrary build plugin configurations.
51   */
52  public class MavenProjectRewriter implements ProjectRewriter
53  {
54      private static final ImmutableSet<String> AMPS_PLUGIN_IDS =
55          ImmutableSet.of("maven-amps-plugin",
56                          "maven-bamboo-plugin",
57                          "maven-confluence-plugin",
58                          "maven-crowd-plugin",
59                          "maven-fecru-plugin",
60                          "maven-jira-plugin",
61                          "maven-refapp-plugin");
62      
63      private final Model model;
64      private final File pom;
65      private final Log log;
66      
67      public MavenProjectRewriter(MavenProject project, Log log)
68      {
69          this.model = checkNotNull(project, "project").getModel();
70          this.pom = project.getFile();
71          this.log = checkNotNull(log, "log");
72      }
73  
74      public MavenProjectRewriter(Model model, File pom)
75      {
76          this.model = checkNotNull(model, "model");
77          this.pom = checkNotNull(pom, "pom");
78          this.log = new SystemStreamLog();
79      }
80      
81      @Override
82      public void applyChanges(PluginProjectChangeset changes) throws Exception
83      {
84          boolean modifyPom = false;
85  
86          modifyPom |= applyDependencyChanges(changes.getItems(ArtifactDependency.class));
87          modifyPom |= applyMavenPluginChanges(changes.getItems(MavenPlugin.class));
88          modifyPom |= applyBundleInstructionChanges(changes.getItems(BundleInstruction.class));
89          modifyPom |= applyPluginArtifactChanges(changes.getItems(com.atlassian.plugins.codegen.PluginArtifact.class));
90          modifyPom |= applyAmpsSystemPropertyChanges(changes.getItems(AmpsSystemPropertyVariable.class));
91  
92          if (modifyPom)
93          {
94              XmlStreamWriter writer = null;
95              try
96              {
97                  writer = new XmlStreamWriter(pom);
98                  PomWriter.write(writer, model, true);
99              }
100             catch (IOException e)
101             {
102                 log.warn("Unable to write plugin-module dependencies to pom.xml", e);
103             }
104             finally
105             {
106                 if (writer != null)
107                 {
108                     IOUtils.closeQuietly(writer);
109                 }
110             }
111         }
112     }
113 
114     @SuppressWarnings("unchecked")
115     private boolean applyDependencyChanges(Iterable<ArtifactDependency> dependencies)
116     {
117         boolean modified = false;
118         List<Dependency> originalDependencies = model.getDependencies();
119         for (ArtifactDependency descriptor : dependencies)
120         {
121             boolean alreadyExists = any(originalDependencies, dependencyArtifactId(descriptor.getGroupAndArtifactId()));
122             if (!alreadyExists)
123             {
124                 modified = true;
125 
126                 Dependency newDependency = new Dependency();
127                 newDependency.setGroupId(descriptor.getGroupAndArtifactId().getGroupId().get());
128                 newDependency.setArtifactId(descriptor.getGroupAndArtifactId().getArtifactId());
129                 newDependency.setVersion(descriptor.getVersionId().getVersionOrPropertyPlaceholder().get());
130                 createVersionPropertyIfNecessary(descriptor.getVersionId());
131                 newDependency.setScope(descriptor.getScope().name().toLowerCase());
132 
133                 model.addDependency(newDependency);
134             }
135         }
136         return modified;
137     }
138 
139     private void createVersionPropertyIfNecessary(VersionId versionId)
140     {
141         for (String p : versionId.getPropertyName())
142         {
143             if (!model.getProperties().containsKey(p))
144             {
145                 model.addProperty(p, versionId.getVersion().getOrElse(""));
146             }
147         }
148     }
149     
150     private boolean applyMavenPluginChanges(Iterable<MavenPlugin> mavenPlugins) throws Exception
151     {
152         boolean modified = false;
153         @SuppressWarnings("unchecked")
154         List<Plugin> originalPlugins = model.getBuild().getPlugins();
155         for (MavenPlugin descriptor : mavenPlugins)
156         {
157             Document fragDoc = DocumentHelper.parseText("<root>" + descriptor.getXmlContent() + "</root>");
158             Predicate<Plugin> match = pluginArtifactId(descriptor.getGroupAndArtifactId());
159             if (Iterables.any(originalPlugins, match))
160             {
161                 modified |= mergeMavenPluginConfig(Iterables.find(originalPlugins, match), fragDoc.getRootElement());
162             }
163             else
164             {
165                 originalPlugins.add(toMavenPlugin(descriptor, fragDoc.getRootElement()));
166                 modified = true;
167             }
168         }
169         return modified;
170     }
171     
172     private static boolean mergeMavenPluginConfig(Plugin plugin, Element paramsDesc)
173     {
174         boolean modified = false;
175         for (Object node : paramsDesc.selectNodes("executions/execution"))
176         {
177             Element executionDesc = (Element) node;
178             if (!plugin.getExecutionsAsMap().containsKey(executionDesc.elementTextTrim("id")))
179             {
180                 plugin.addExecution(toMavenPluginExecution(executionDesc));
181                 modified = true;
182             }
183         }
184         return modified;
185     }
186     
187     private Plugin toMavenPlugin(MavenPlugin descriptor, Element paramsDesc)
188     {
189         Plugin p = new Plugin();
190         p.setGroupId(descriptor.getGroupAndArtifactId().getGroupId().getOrElse((String)null));
191         p.setArtifactId(descriptor.getGroupAndArtifactId().getArtifactId());
192         if (descriptor.getVersionId().isDefined())
193         {
194             p.setVersion(descriptor.getVersionId().getVersionOrPropertyPlaceholder().get());
195             createVersionPropertyIfNecessary(descriptor.getVersionId());
196         }
197         p.setExtensions("true".equals(paramsDesc.elementText("extensions")));
198         for (Object configNode : paramsDesc.selectNodes("configuration"))
199         {
200             p.setConfiguration(toXpp3Dom((Element)configNode));
201         }
202         for (Object execNode : paramsDesc.selectNodes("executions/execution"))
203         {
204             p.addExecution(toMavenPluginExecution((Element)execNode));
205         }
206         return p;
207     }
208 
209     private static PluginExecution toMavenPluginExecution(Element executionDesc)
210     {
211         PluginExecution pe = new PluginExecution();
212         pe.setId(executionDesc.elementTextTrim("id"));
213         pe.setPhase(executionDesc.elementTextTrim("phase"));
214         for (Object goalNode : executionDesc.selectNodes("goals/goal"))
215         {
216             pe.addGoal(((Node)goalNode).getText());
217         }
218         for (Object configNode : executionDesc.selectNodes("configuration"))
219         {
220             pe.setConfiguration(toXpp3Dom((Element)configNode));
221         }
222         return pe;
223     }
224     
225     private boolean applyBundleInstructionChanges(Iterable<BundleInstruction> instructions)
226     {
227         Xpp3Dom configRoot = getAmpsPluginConfiguration();
228         boolean modified = false;
229         Xpp3Dom instructionsRoot = getOrCreateElement(configRoot, "instructions");
230         for (BundleInstruction instruction : instructions)
231         {
232             String categoryName = instruction.getCategory().getElementName();
233             Xpp3Dom categoryElement = getOrCreateElement(instructionsRoot, categoryName);
234             String body = categoryElement.getValue();
235             Iterable<BundleInstruction> instructionList = parseInstructions(instruction.getCategory(), (body == null) ? "" : body);
236             if (any(instructionList, bundleInstructionPackageName(instruction.getPackageName())))
237             {
238                 continue;
239             }
240             Iterable<BundleInstruction> newList = new InstructionPackageOrdering().sortedCopy(
241                 concat(instructionList, ImmutableList.of(instruction)));
242             categoryElement.setValue(writeInstructions(newList));
243             modified = true;
244         }
245         return modified;
246     }
247     
248     private static Iterable<BundleInstruction> parseInstructions(BundleInstruction.Category category, String body)
249     {
250         ImmutableList.Builder<BundleInstruction> ret = ImmutableList.builder();
251         for (String instructionLine : body.split(","))
252         {
253             String[] instructionParts = instructionLine.trim().split(";");
254             if (instructionParts.length == 1)
255             {
256                 ret.add(new BundleInstruction(category, instructionParts[0], none(String.class)));
257             }
258             else if (instructionParts.length == 2 && instructionParts[1].startsWith("version=\"") && instructionParts[1].endsWith("\""))
259             {
260                 String version = instructionParts[1].substring(9, instructionParts[1].length() - 1);
261                 ret.add(new BundleInstruction(category, instructionParts[0], some(version)));
262             }
263         }
264         return ret.build();
265     }
266     
267     private static String writeInstructions(Iterable<BundleInstruction> instructions)
268     {
269         StringBuilder ret = new StringBuilder("\n");
270         for (BundleInstruction instruction : instructions)
271         {
272             if (ret.length() > 1)
273             {
274                 ret.append(",\n");
275             }
276             ret.append(instruction.getPackageName());
277             for (String version : instruction.getVersion())
278             {
279                 ret.append(";version=\"").append(version).append("\"");
280             }
281         }
282         return ret.append("\n").toString();
283     }
284     
285     private boolean applyPluginArtifactChanges(Iterable<com.atlassian.plugins.codegen.PluginArtifact> pluginArtifacts)
286     {
287         Xpp3Dom configRoot = getAmpsPluginConfiguration();
288         boolean modified = false;
289         for (com.atlassian.plugins.codegen.PluginArtifact p : pluginArtifacts)
290         {
291             Xpp3Dom artifactsRoot = getOrCreateElement(configRoot, p.getType().getElementName() + "s");
292             List<Xpp3Dom> existingItems = ImmutableList.copyOf(artifactsRoot.getChildren(p.getType().getElementName()));
293             if (!any(existingItems, artifactElement(p.getGroupAndArtifactId())))
294             {
295                 artifactsRoot.addChild(toArtifactElement(p));
296                 modified = true;
297             }
298         }
299         return modified;
300     }
301 
302     private boolean applyAmpsSystemPropertyChanges(Iterable<AmpsSystemPropertyVariable> propertyVariables)
303     {
304         Xpp3Dom configRoot = getAmpsPluginConfiguration();
305         boolean modified = false;
306         for (AmpsSystemPropertyVariable propertyVariable : propertyVariables)
307         {
308             Xpp3Dom variablesRoot = getOrCreateElement(configRoot, "systemPropertyVariables");
309             if (variablesRoot.getChild(propertyVariable.getName()) == null)
310             {
311                 Xpp3Dom variableElement = new Xpp3Dom(propertyVariable.getName());
312                 variableElement.setValue(propertyVariable.getValue());
313                 variablesRoot.addChild(variableElement);
314                 modified = true;
315             }
316         }
317         return modified;
318     }
319     
320     private Xpp3Dom toArtifactElement(com.atlassian.plugins.codegen.PluginArtifact pluginArtifact)
321     {
322         Xpp3Dom ret = new Xpp3Dom(pluginArtifact.getType().getElementName());
323         for (String groupId : pluginArtifact.getGroupAndArtifactId().getGroupId())
324         {
325             Xpp3Dom ge = new Xpp3Dom("groupId");
326             ge.setValue(groupId);
327             ret.addChild(ge);
328         }
329         Xpp3Dom ae = new Xpp3Dom("artifactId");
330         ae.setValue(pluginArtifact.getGroupAndArtifactId().getArtifactId());
331         ret.addChild(ae);
332         if (pluginArtifact.getVersionId().isDefined())
333         {
334             Xpp3Dom ve = new Xpp3Dom("version");
335             ve.setValue(pluginArtifact.getVersionId().getVersionOrPropertyPlaceholder().get());
336             createVersionPropertyIfNecessary(pluginArtifact.getVersionId());
337             ret.addChild(ve);
338         }
339         return ret;
340     }
341     
342     @SuppressWarnings("unchecked")
343     private Plugin findAmpsPlugin()
344     {
345         for (Plugin p : (List<Plugin>) model.getBuild().getPlugins())
346         {
347             if (p.getGroupId().equals("com.atlassian.maven.plugins")
348                 && AMPS_PLUGIN_IDS.contains(p.getArtifactId()))
349             {
350                 return p;
351             }
352         }
353         throw new IllegalStateException("Could not find AMPS plugin element in POM");
354     }
355 
356     private Xpp3Dom getAmpsPluginConfiguration()
357     {
358         Plugin ampsPlugin = findAmpsPlugin();
359         Xpp3Dom configRoot = (Xpp3Dom) ampsPlugin.getConfiguration();
360         if (configRoot == null)
361         {
362             configRoot = new Xpp3Dom("configuration");
363             ampsPlugin.setConfiguration(configRoot);
364         }
365         return configRoot;
366     }
367     
368     private static Xpp3Dom getOrCreateElement(Xpp3Dom container, String name)
369     {
370         Xpp3Dom ret = container.getChild(name);
371         if (ret == null)
372         {
373             ret = new Xpp3Dom(name);
374             container.addChild(ret);
375         }
376         return ret;
377     }
378     
379     private static Xpp3Dom toXpp3Dom(Element dom4JElement)
380     {
381         try
382         {
383             return Xpp3DomBuilder.build(new StringReader(dom4JElement.asXML()));
384         }
385         catch (Exception e)
386         {
387             // should never fail to parse XML that was already parsed by dom4J
388             throw new IllegalStateException();
389         }
390     }
391     
392     private static Predicate<Dependency> dependencyArtifactId(final ArtifactId artifactId)
393     {
394         return new Predicate<Dependency>()
395         {
396             public boolean apply(Dependency d)
397             {
398                 return (artifactId.getGroupId().equals(option(d.getGroupId()))
399                         && artifactId.getArtifactId().equals(d.getArtifactId()));
400             }
401         };
402     }
403 
404     private static Predicate<Plugin> pluginArtifactId(final ArtifactId artifactId)
405     {
406         return new Predicate<Plugin>()
407         {
408             public boolean apply(Plugin p)
409             {
410                 return artifactId.getArtifactId().equals(p.getArtifactId())
411                     && (artifactId.getGroupId().equals(option(p.getGroupId()))
412                         || (!artifactId.getGroupId().isDefined() && "org.apache.maven.plugins".equals(p.getGroupId())));
413             }
414         };
415     }
416 
417     private static Predicate<Xpp3Dom> artifactElement(final ArtifactId artifactId)
418     {
419         return new Predicate<Xpp3Dom>()
420         {
421             public boolean apply(Xpp3Dom e)
422             {
423                 return (e.getChild("artifactId") != null)
424                     && e.getChild("artifactId").getValue().equals(artifactId.getArtifactId())
425                     && (((e.getChild("groupId") == null) && !artifactId.getGroupId().isDefined())
426                         || (e.getChild("groupId") != null) && artifactId.getGroupId().equals(some(e.getChild("groupId").getValue())));
427             }
428         };
429     }
430 
431     private static Predicate<BundleInstruction> bundleInstructionPackageName(final String packageName)
432     {
433         return new Predicate<BundleInstruction>()
434         {
435             public boolean apply(BundleInstruction i)
436             {
437                 return i.getPackageName().equals(packageName);
438             }
439         };
440     }
441     
442     private static class InstructionPackageOrdering extends Ordering<BundleInstruction>
443     {
444         @Override
445         public int compare(BundleInstruction first, BundleInstruction second)
446         {
447             return first.getPackageName().compareTo(second.getPackageName());
448         }
449     }
450 }