Skip to content

Commit 054a842

Browse files
authored
[custom] New check on bundle feature.xml file (#395)
* Changed missing feature warn to debug With the features moved to the addons it will not find the feature.xml and report this as a warn on each binding, while it should not give this warning as it ok. * [custom] Add check for feature.xml The following checks are added: 1) If there is a feature.xml at all. 2) If the name in <features tag is as expected. 3) If the name in <feature tag is as expected. 4) If the value in binding <bundle tag is as expected. Signed-off-by: Hilbrand Bouwkamp <hilbrand@h72.nl>
1 parent 4a6198a commit 054a842

15 files changed

Lines changed: 489 additions & 3 deletions

File tree

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/**
2+
* Copyright (c) 2010-2021 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.tools.analysis.checkstyle;
14+
15+
import static org.openhab.tools.analysis.checkstyle.api.CheckConstants.*;
16+
17+
import java.io.File;
18+
import java.io.IOException;
19+
import java.nio.charset.StandardCharsets;
20+
import java.text.MessageFormat;
21+
import java.util.ArrayList;
22+
import java.util.LinkedHashMap;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.Map.Entry;
26+
import java.util.function.Function;
27+
28+
import javax.xml.xpath.XPathConstants;
29+
import javax.xml.xpath.XPathExpression;
30+
import javax.xml.xpath.XPathExpressionException;
31+
32+
import org.eclipse.jdt.annotation.NonNullByDefault;
33+
import org.eclipse.jdt.annotation.Nullable;
34+
import org.openhab.tools.analysis.checkstyle.api.AbstractStaticCheck;
35+
import org.slf4j.Logger;
36+
import org.slf4j.LoggerFactory;
37+
import org.w3c.dom.Document;
38+
import org.w3c.dom.Node;
39+
import org.w3c.dom.NodeList;
40+
41+
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
42+
import com.puppycrawl.tools.checkstyle.api.FileText;
43+
44+
/**
45+
* Checks if an add-on has a valid feature.xml file.
46+
*
47+
* @author Hilbrand Bouwkamp - Initial contribution
48+
*/
49+
@NonNullByDefault
50+
public class KarafAddonFeatureCheck extends AbstractStaticCheck {
51+
52+
public static final String FEATURE_XML = "feature.xml";
53+
public static final String FEATURE_XML_PATH = "src/main/feature/" + FEATURE_XML;
54+
public static final String MSG_MISSING_FEATURE_XML = "Missing feature file {0}";
55+
public static final String MSG_FEATURES_NAME_INVALID = "Invalid features name, expected name starting with: {0}";
56+
public static final String MSG_FEATURE_NAME_INVALID = "Invalid feature name, expected name: {0}";
57+
public static final String BUNDLE_VALUE = "mvn:org.openhab.addons.bundles/{0}/$'{'project.version'}'";
58+
public static final String MSG_BUNDLE_INVALID = "Invalid or missing bundle entry. Expected <bundle start-level=\"80\">{0}</bundle>";
59+
60+
private static final String FEATURES_NAME_EXPRESSION = "//features[@name]/@name";
61+
private static final String FEATURES_SEARCH = "<features";
62+
private static final String FEATURE_NAME_EXPRESSION = "//features/feature[@name]/@name";
63+
private static final String FEATURE_SEARCH = "<feature ";
64+
private static final String BUNDLE_EXPRESSION = "//features/feature/bundle/text()";
65+
private static final String BUNDLE_SEARCH = "mvn:org.openhab.addons.bundles";
66+
67+
private static final String POM_ARTIFACT_ID_XPATH_EXPRESSION = "//project/artifactId/text()";
68+
private static final String POM_PARENT_ARTIFACT_ID = "org.openhab.addons.reactor.bundles";
69+
private static final String POM_PARENT_ARTIFACT_ID_XPATH_EXPRESSION = "//project/parent/artifactId/text()";
70+
71+
private final Logger logger = LoggerFactory.getLogger(KarafAddonFeatureCheck.class);
72+
73+
private final Map<String, String> featureNamePatternsMap = new LinkedHashMap<>();
74+
private final List<String> excludeFeatureNamesList = new ArrayList<>();
75+
76+
public KarafAddonFeatureCheck() {
77+
setFileExtensions(XML_EXTENSION);
78+
}
79+
80+
/**
81+
* Comma separated list of binding names (name after pattern applied) of feature names that will be ignored for
82+
* checking.
83+
*
84+
* @param excludeFeatureNames command separated list
85+
*/
86+
public void setExcludeFeatureNames(String excludeFeatureNames) {
87+
if (excludeFeatureNames.trim().isBlank()) {
88+
return;
89+
}
90+
for (String exclude : excludeFeatureNames.split(",")) {
91+
excludeFeatureNamesList.add(exclude);
92+
}
93+
}
94+
95+
/**
96+
* Comma separated list of key value pairs (separated by colon) transform the calculated expected feature name in
97+
* the actual feature name.
98+
* For example openhab-transform-map:openhab-transformation-map.
99+
* the openhab-transform-map is as expected as derived from the artifactId, but all bundles use transformation.
100+
* So with the pattern expected feature name can be constructed.
101+
*
102+
* @param featureNamePatterns list of key/value pairs of patterns
103+
*/
104+
public void setFeatureNamePatterns(String featureNamePatterns) {
105+
if (featureNamePatterns.trim().isBlank()) {
106+
return;
107+
}
108+
for (String pattern : featureNamePatterns.split(",")) {
109+
final String[] keypair = pattern.split(":");
110+
111+
if (keypair.length == 2) {
112+
featureNamePatternsMap.put(keypair[0], keypair[1]);
113+
} else {
114+
logger.warn("{} check pattern for option featureNamePatterns is invalid. Value set: {}",
115+
getClass().getName(), featureNamePatterns);
116+
}
117+
}
118+
}
119+
120+
@Override
121+
protected void processFiltered(@Nullable File file, @Nullable FileText fileText) throws CheckstyleException {
122+
if (file == null || fileText == null) {
123+
return;
124+
}
125+
switch (file.getName()) {
126+
case POM_XML_FILE_NAME:
127+
checkMissingFeatureFile(file, fileText);
128+
break;
129+
case FEATURE_XML:
130+
checkFeatureFile(file, fileText);
131+
break;
132+
}
133+
}
134+
135+
private void checkMissingFeatureFile(File file, FileText fileText) throws CheckstyleException {
136+
final String artifactId = getArtifactId(fileText);
137+
138+
if (artifactId == null) {
139+
logger.warn("{} will be skipped. Could not find Maven group ID (parent group ID) or artifact ID in {}",
140+
getClass().getSimpleName(), file.getAbsolutePath());
141+
return;
142+
}
143+
final File featureFile = new File(file.getParent(), FEATURE_XML_PATH);
144+
145+
if (!featureFile.exists()) {
146+
logMessage(featureFile.toString(), 0, FEATURE_XML,
147+
MessageFormat.format(MSG_MISSING_FEATURE_XML, featureFile));
148+
}
149+
}
150+
151+
private void checkFeatureFile(File featureFile, FileText fileText) throws CheckstyleException {
152+
try {
153+
final String featureFileString = featureFile.getAbsoluteFile().toString();
154+
final FileText pomFile = new FileText(
155+
new File(featureFileString.replace(FEATURE_XML_PATH, ""), POM_XML_FILE_NAME),
156+
StandardCharsets.UTF_8.name());
157+
final String artifactId = getArtifactId(pomFile);
158+
159+
if (artifactId == null) {
160+
logger.debug("Ignore check on feature.xml with no bundle specific pom.xml: {}", featureFileString);
161+
} else {
162+
final Document featureXML = parseDomDocumentFromFile(fileText);
163+
164+
checkFeatures(featureFile, artifactId, fileText, featureXML);
165+
checkFeature(featureFile, artifactId, fileText, featureXML);
166+
checkBundle(featureFile, artifactId, fileText, featureXML);
167+
}
168+
} catch (IOException e) {
169+
logger.error("Could not read {}", FEATURE_XML_PATH);
170+
}
171+
}
172+
173+
private void checkFeatures(File featureFile, String artifactId, FileText fileText, Document featureXML) {
174+
checkName(featureFile, artifactId, fileText, featureXML, FEATURES_NAME_EXPRESSION,
175+
name -> !artifactId.equals(name.substring(0, name.indexOf('-'))), MSG_FEATURES_NAME_INVALID,
176+
FEATURES_SEARCH);
177+
}
178+
179+
private void checkFeature(File featureFile, String artifactId, FileText fileText, Document featureXML) {
180+
final String featureName = adaptedFeatureName(artifactId);
181+
182+
for (String exclude : excludeFeatureNamesList) {
183+
if (exclude.equals(featureName)) {
184+
logger.debug("Ignore check on feature name {}", featureName);
185+
return;
186+
}
187+
}
188+
checkName(featureFile, featureName, fileText, featureXML, FEATURE_NAME_EXPRESSION,
189+
name -> !featureName.equals(name), MSG_FEATURE_NAME_INVALID, FEATURE_SEARCH);
190+
}
191+
192+
private String adaptedFeatureName(String artifactId) {
193+
String featureName = artifactId.substring(4).replaceAll("\\.", "-");
194+
195+
for (Entry<String, String> entry : featureNamePatternsMap.entrySet()) {
196+
featureName = featureName.replace(entry.getKey(), entry.getValue());
197+
}
198+
return featureName;
199+
}
200+
201+
private void checkName(File featureFile, String expectedName, FileText fileText, Document featureXML,
202+
String expression, Function<String, Boolean> checkFunction, String message, String lineSearchString) {
203+
final Node featuresName = getFirstNode(featureXML, expression);
204+
final String errorMessage = MessageFormat.format(message, expectedName);
205+
206+
if (featuresName == null) {
207+
logMessage(featureFile.getAbsolutePath(), 0, featureFile.getName(), errorMessage);
208+
} else {
209+
String name = featuresName.getNodeValue();
210+
211+
if (name == null || checkFunction.apply(name)) {
212+
logMessage(featureFile.getAbsolutePath(),
213+
findLineNumberSafe(fileText, lineSearchString, 0, errorMessage), featureFile.getName(),
214+
errorMessage);
215+
}
216+
}
217+
}
218+
219+
private void checkBundle(File featureFile, String artifactId, FileText fileText, Document document) {
220+
final String errorMessage = MessageFormat.format(MSG_BUNDLE_INVALID, BUNDLE_VALUE.replace("{0}", artifactId));
221+
222+
try {
223+
final XPathExpression artifactIdExpression = compileXPathExpression(BUNDLE_EXPRESSION);
224+
final NodeList bundles = ((NodeList) artifactIdExpression.evaluate(document, XPathConstants.NODESET));
225+
final String expectedBundleValue = MessageFormat.format(BUNDLE_VALUE, artifactId);
226+
boolean found = false;
227+
228+
for (int i = 0; i < bundles.getLength(); i++) {
229+
final Node item = bundles.item(i);
230+
if (expectedBundleValue.equals(item.getNodeValue())) {
231+
found = true;
232+
break;
233+
}
234+
}
235+
if (!found) {
236+
logMessage(featureFile.getAbsolutePath(), findLineNumberSafe(fileText, BUNDLE_SEARCH, 0, errorMessage),
237+
featureFile.getName(), errorMessage);
238+
}
239+
} catch (CheckstyleException | XPathExpressionException e) {
240+
logMessage(featureFile.getAbsolutePath(), 0, featureFile.getName(), errorMessage);
241+
}
242+
}
243+
244+
private @Nullable String getArtifactId(FileText fileText) throws CheckstyleException {
245+
final Document documentXML = parseDomDocumentFromFile(fileText);
246+
final Node artifactId = getFirstNode(documentXML, POM_ARTIFACT_ID_XPATH_EXPRESSION);
247+
final Node parentArtifactId = getFirstNode(documentXML, POM_PARENT_ARTIFACT_ID_XPATH_EXPRESSION);
248+
249+
return artifactId == null || parentArtifactId == null
250+
|| !POM_PARENT_ARTIFACT_ID.equals(parentArtifactId.getNodeValue()) ? null : artifactId.getNodeValue();
251+
}
252+
253+
private @Nullable Node getFirstNode(Document document, String xpathExpression) {
254+
try {
255+
final XPathExpression artifactIdExpression = compileXPathExpression(xpathExpression);
256+
257+
return ((NodeList) artifactIdExpression.evaluate(document, XPathConstants.NODESET)).item(0);
258+
} catch (CheckstyleException | XPathExpressionException e) {
259+
logger.error("Could not evaluate XPath expression {}", xpathExpression, e);
260+
return null;
261+
}
262+
}
263+
}

custom-checks/checkstyle/src/main/java/org/openhab/tools/analysis/checkstyle/KarafFeatureCheck.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ protected void processFiltered(File file, FileText fileText) throws CheckstyleEx
8787
Path featurePath = resolveRecursively(file.toPath(), Paths.get(singlePath));
8888

8989
if (featurePath == null) {
90-
logger.warn("Could not find file feature file {}", singlePath);
90+
logger.debug("Could not find file feature file {}", singlePath);
9191
continue;
9292
}
9393

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Copyright (c) 2010-2021 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.tools.analysis.checkstyle.test;
14+
15+
import static org.openhab.tools.analysis.checkstyle.KarafAddonFeatureCheck.*;
16+
import static org.openhab.tools.analysis.checkstyle.api.CheckConstants.POM_XML_FILE_NAME;
17+
18+
import java.io.File;
19+
import java.text.MessageFormat;
20+
21+
import org.apache.commons.lang.ArrayUtils;
22+
import org.junit.Test;
23+
import org.openhab.tools.analysis.checkstyle.KarafAddonFeatureCheck;
24+
import org.openhab.tools.analysis.checkstyle.api.AbstractStaticCheckTest;
25+
26+
import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
27+
28+
/**
29+
* Tests for {@link KarafAddonFeatureCheck}
30+
*
31+
* @author Hilbrand Bouwkamp - Initial contribution
32+
*/
33+
public class KarafAddonFeatureCheckTest extends AbstractStaticCheckTest {
34+
35+
private static final String ARTIFACT_ID = "org.openhab.binding.example";
36+
private static final DefaultConfiguration CONFIGURATION = createModuleConfig(KarafAddonFeatureCheck.class);
37+
38+
@Override
39+
protected String getPackageLocation() {
40+
return "checkstyle/karafAddonFeatureCheck";
41+
}
42+
43+
@Test
44+
public void testValid() throws Exception {
45+
verify("valid", ArrayUtils.EMPTY_STRING_ARRAY);
46+
}
47+
48+
@Test
49+
public void testMissingFeatureFile() throws Exception {
50+
final File featureFile = new File(getPath("missingFeature"), FEATURE_XML_PATH);
51+
52+
verify(createChecker(CONFIGURATION), getPath("missingFeature" + File.separator + POM_XML_FILE_NAME),
53+
featureFile.getAbsolutePath(),
54+
generateExpectedMessages(0, MessageFormat.format(MSG_MISSING_FEATURE_XML, featureFile)));
55+
}
56+
57+
@Test
58+
public void testInvalidBundleName() throws Exception {
59+
verify("invalidBundle", generateExpectedMessages(7,
60+
MessageFormat.format(MSG_BUNDLE_INVALID, MessageFormat.format(BUNDLE_VALUE, ARTIFACT_ID))));
61+
}
62+
63+
@Test
64+
public void testInvalidFeaturesName() throws Exception {
65+
verify("invalidFeaturesName",
66+
generateExpectedMessages(2, MessageFormat.format(MSG_FEATURES_NAME_INVALID, ARTIFACT_ID)));
67+
}
68+
69+
@Test
70+
public void testInvalidFeatureName() throws Exception {
71+
verify("invalidFeatureName", generateExpectedMessages(5,
72+
MessageFormat.format(MSG_FEATURE_NAME_INVALID, ARTIFACT_ID.replaceAll("\\.", "-").substring(4))));
73+
}
74+
75+
@Test
76+
public void testPatternFeatureName() throws Exception {
77+
DefaultConfiguration config = createModuleConfig(KarafAddonFeatureCheck.class);
78+
config.addAttribute("featureNamePatterns", "openhab-binding-example:org.openhab.binding.someother");
79+
verify(config, getPath("invalidFeatureName" + File.separator + FEATURE_XML_PATH),
80+
ArrayUtils.EMPTY_STRING_ARRAY);
81+
}
82+
83+
@Test
84+
public void testExcludedFeatureName() throws Exception {
85+
DefaultConfiguration config = createModuleConfig(KarafAddonFeatureCheck.class);
86+
config.addAttribute("featureNamePatterns", "openhab-binding-example:org.openhab.binding.someother");
87+
config.addAttribute("excludeFeatureNames", "org.openhab.binding.someother");
88+
verify(config, getPath("invalidFeatureName" + File.separator + FEATURE_XML_PATH),
89+
ArrayUtils.EMPTY_STRING_ARRAY);
90+
}
91+
92+
private void verify(String fileDirectory, String[] expectedMessages) throws Exception {
93+
verify(CONFIGURATION, getPath(fileDirectory + File.separator + FEATURE_XML_PATH), expectedMessages);
94+
}
95+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
6+
<parent>
7+
<groupId>org.openhab.addons.bundles</groupId>
8+
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
9+
<version>3.0.0-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>org.openhab.binding.example</artifactId>
13+
14+
<name>openHAB Add-ons :: Bundles :: Example Binding</name>
15+
16+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<features name="org.openhab.binding.example-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
3+
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${project.version}/xml/features</repository>
4+
5+
<feature name="openhab-binding-example" description="Example Binding" version="${project.version}">
6+
<feature>openhab-runtime-base</feature>
7+
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.wrong/${project.version}</bundle>
8+
</feature>
9+
</features>

0 commit comments

Comments
 (0)