Skip to content

Commit 8336d09

Browse files
committed
Add enforcer rule to require elements in the POM
Previously, we noted in the description of our base POMs that inheriting projects needed to override a boatload of elements. But nothing enforced it, so everyone was free to ignore that advice and instead unknowingly inherit the configuration of the parent, which was often incorrect. This rule makes the requirement to override all of these elements iron-clad, avoiding this sort of confusion henceforth.
1 parent 4319f12 commit 8336d09

File tree

1 file changed

+203
-0
lines changed

1 file changed

+203
-0
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
* #%L
3+
* A plugin for managing SciJava-based projects.
4+
* %%
5+
* Copyright (C) 2014 - 2015 Board of Regents of the University of
6+
* Wisconsin-Madison.
7+
* %%
8+
* Redistribution and use in source and binary forms, with or without
9+
* modification, are permitted provided that the following conditions are met:
10+
*
11+
* 1. Redistributions of source code must retain the above copyright notice,
12+
* this list of conditions and the following disclaimer.
13+
* 2. Redistributions in binary form must reproduce the above copyright notice,
14+
* this list of conditions and the following disclaimer in the documentation
15+
* and/or other materials provided with the distribution.
16+
*
17+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
21+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
* POSSIBILITY OF SUCH DAMAGE.
28+
* #L%
29+
*/
30+
31+
package org.scijava.maven.plugin.enforcer;
32+
33+
import java.io.File;
34+
import java.io.IOException;
35+
36+
import javax.xml.parsers.DocumentBuilder;
37+
import javax.xml.parsers.DocumentBuilderFactory;
38+
import javax.xml.parsers.ParserConfigurationException;
39+
import javax.xml.xpath.XPath;
40+
import javax.xml.xpath.XPathConstants;
41+
import javax.xml.xpath.XPathExpressionException;
42+
import javax.xml.xpath.XPathFactory;
43+
44+
import org.apache.maven.enforcer.rule.api.EnforcerRule;
45+
import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
46+
import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
47+
import org.apache.maven.plugin.logging.Log;
48+
import org.apache.maven.project.MavenProject;
49+
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
50+
import org.codehaus.plexus.util.StringUtils;
51+
import org.w3c.dom.Document;
52+
import org.w3c.dom.NodeList;
53+
import org.xml.sax.SAXException;
54+
55+
/**
56+
* This rule checks that particular XML elements are set in the project POM
57+
* (<em>not</em> inherited from an ancestor!).
58+
*
59+
* @author Curtis Rueden
60+
*/
61+
public class RequireElements implements EnforcerRule {
62+
63+
/** The required elements. Must be given. */
64+
private String[] elements;
65+
66+
private Document doc;
67+
private XPath xpath;
68+
69+
private final String ruleName = //
70+
StringUtils.lowercaseFirstLetter(getClass().getSimpleName());
71+
72+
// -- EnforcerRule methods --
73+
74+
/**
75+
* Execute the rule.
76+
*
77+
* @param helper the helper
78+
* @throws EnforcerRuleException the enforcer rule exception
79+
*/
80+
@Override
81+
public void execute(final EnforcerRuleHelper helper)
82+
throws EnforcerRuleException
83+
{
84+
final Log log = helper.getLog();
85+
86+
final MavenProject project = getMavenProject(helper);
87+
88+
// Validate rule inputs.
89+
if (elements == null || elements.length == 0) {
90+
fail("no elements were specified");
91+
}
92+
for (final String element : elements) {
93+
if (element.matches("[^A-Za-z0-9_]")) {
94+
fail("invalid character in element name '" + element + "'");
95+
}
96+
}
97+
98+
// Locate the project's POM file.
99+
final File pomFile = project.getFile();
100+
if (pomFile == null) {
101+
fail("cannot locate project POM");
102+
}
103+
104+
// Parse the project POM directly. We do this, rather than leaning on the
105+
// MavenProject, because we do _not_ want to interpolate the POM. Instead,
106+
// we want to know if this specific POM includes the requisite element.
107+
try {
108+
doc = loadXML(pomFile);
109+
}
110+
catch (final ParserConfigurationException exc) {
111+
log.warn("Cannot parse project POM", exc);
112+
return;
113+
}
114+
catch (final SAXException exc) {
115+
log.warn("Cannot parse project POM", exc);
116+
return;
117+
}
118+
catch (final IOException exc) {
119+
log.warn("Cannot parse project POM", exc);
120+
return;
121+
}
122+
123+
xpath = XPathFactory.newInstance().newXPath();
124+
final StringBuilder errors = new StringBuilder();
125+
for (final String element : elements) {
126+
// Find matching element(s).
127+
final NodeList nodes = xpath("//project/" + element);
128+
if (nodes == null || nodes.getLength() <= 0) {
129+
errors.append("* " + element + ": element is missing\n");
130+
continue;
131+
}
132+
133+
// Ensure first element has actual content beneath it.
134+
final String text = nodes.item(0).getTextContent();
135+
if (text == null || text.trim().isEmpty()) {
136+
errors.append("* " + element + ": element has no content\n");
137+
}
138+
}
139+
if (errors.length() > 0) {
140+
throw new EnforcerRuleException(
141+
"The following required elements have errors:\n" + errors);
142+
}
143+
}
144+
145+
@Override
146+
public String getCacheId() {
147+
return "0";
148+
}
149+
150+
@Override
151+
public boolean isCacheable() {
152+
return false;
153+
}
154+
155+
@Override
156+
public boolean isResultValid(final EnforcerRule cachedRule) {
157+
return false;
158+
}
159+
160+
// -- Helper methods --
161+
162+
private void fail(final String message) throws EnforcerRuleException {
163+
throw new EnforcerRuleException(ruleName + ": " + message);
164+
}
165+
166+
private MavenProject getMavenProject(final EnforcerRuleHelper helper)
167+
throws EnforcerRuleException
168+
{
169+
try {
170+
return (MavenProject) helper.evaluate("${project}");
171+
}
172+
catch (final ExpressionEvaluationException exc) {
173+
throw new EnforcerRuleException("Unable to get project.", exc);
174+
}
175+
}
176+
177+
private NodeList xpath(final String expression) throws EnforcerRuleException {
178+
final Object o;
179+
try {
180+
o = xpath.evaluate(expression, doc, XPathConstants.NODESET);
181+
}
182+
catch (final XPathExpressionException exc) {
183+
throw new EnforcerRuleException("Cannot parse xpath expression", exc);
184+
}
185+
if (!(o instanceof NodeList)) {
186+
final String className = o == null ? "null" : o.getClass().getName();
187+
fail("Unexpected xpath result type: " + className);
188+
}
189+
return (NodeList) o;
190+
}
191+
192+
private static Document loadXML(final File file)
193+
throws ParserConfigurationException, SAXException, IOException
194+
{
195+
return createBuilder().parse(file.getAbsolutePath());
196+
}
197+
198+
private static DocumentBuilder createBuilder()
199+
throws ParserConfigurationException
200+
{
201+
return DocumentBuilderFactory.newInstance().newDocumentBuilder();
202+
}
203+
}

0 commit comments

Comments
 (0)