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
44
45
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
160
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
187 Element ampsVersionProperty = getOrCreateElement(getOrCreateElement(root, "properties"),"amps.version");
188
189
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
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
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 }