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
42
43
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 }