Skip to content

Commit 9311d13

Browse files
authored
[custom] Added check for label length and label words uppercase (#360)
This adds new checks for OH-INF xml files: - Gives a warning if a label exceeds the length of 20 characters. - Gives an error if a label exceeds the length of 25 characters. - Gives a warning if a label doesn't comply with the all-words-start-with-uppercase-character convention. There are some exceptions to this rule that are handled. Signed-off-by: Hilbrand Bouwkamp <hilbrand@h72.nl>
1 parent fd45fdc commit 9311d13

13 files changed

Lines changed: 587 additions & 46 deletions

File tree

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
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 java.io.File;
16+
import java.text.MessageFormat;
17+
import java.util.Arrays;
18+
import java.util.Collections;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Set;
23+
import java.util.regex.Matcher;
24+
import java.util.regex.Pattern;
25+
import java.util.stream.Collectors;
26+
import java.util.stream.Stream;
27+
28+
import org.openhab.tools.analysis.checkstyle.api.AbstractOhInfXmlCheck;
29+
import org.w3c.dom.Node;
30+
import org.w3c.dom.NodeList;
31+
32+
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
33+
import com.puppycrawl.tools.checkstyle.api.FileText;
34+
import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
35+
36+
/**
37+
* Checks if all words in a label start with an uppercase character and if labels are not to long.
38+
*
39+
* @author Hilbrand Bouwkamp - Initial contribution
40+
*/
41+
public class OhInfXmlLabelCheck extends AbstractOhInfXmlCheck {
42+
43+
private static final String CONFIG_ATTR_KEY = "name";
44+
private static final String THING_ATTR_KEY = "id";
45+
46+
private static final String I18N_PREFIX = "@text/";
47+
48+
private static final String PARAMETER_LABEL_EXPRESSION = "//parameter/label/text()";
49+
private static final String PARAMETER_GROUP_LABEL_EXPRESSION = "//parameter-group/label/text()";
50+
51+
private static final String CHANNEL_LABEL_EXPRESSION = "//channel/label/text()";
52+
private static final String CHANNEL_GROUP_LABEL_EXPRESSION = "//channel-group/label/text()";
53+
private static final String CHANNEL_TYPE_LABEL_EXPRESSION = "//channel-type/label/text()";
54+
private static final String CHANNEL_GROUP_TYPE_LABEL_EXPRESSION = "//channel-group-type/label/text()";
55+
private static final String THING_TYPE_LABEL_EXPRESSION = "//thing-type/label/text()";
56+
57+
/**
58+
* If any of these characters appear in the word when the first character is lowercase the first character should
59+
* not be reported to be changed to uppercase. This is to handle exceptional cases, like brand names, words with
60+
* numbers or units between brackets, e.g. (ms).
61+
*/
62+
private static final Pattern SKIP_PATTERN = Pattern.compile("[A-Z\\d+\\.\\(\\)]");
63+
64+
private static final List<String> ALL_CONFIG_EXPRESSIONS = Arrays.asList(PARAMETER_LABEL_EXPRESSION,
65+
PARAMETER_GROUP_LABEL_EXPRESSION);
66+
private static final List<String> ALL_THING_EXPRESSIONS = Arrays.asList(THING_TYPE_LABEL_EXPRESSION,
67+
CHANNEL_LABEL_EXPRESSION, CHANNEL_GROUP_LABEL_EXPRESSION, CHANNEL_TYPE_LABEL_EXPRESSION,
68+
CHANNEL_GROUP_TYPE_LABEL_EXPRESSION);
69+
private static final List<String> ALL_THING_CONFIG_EXPRESSIONS = Collections
70+
.singletonList(PARAMETER_LABEL_EXPRESSION);
71+
72+
private static final Pattern TYPE_PATTERN = Pattern.compile("//([^\\/]+)");
73+
private static final Pattern LABEL_PATTERN = Pattern.compile("<label>([^<]+)</label>");
74+
75+
public static final String MESSAGE_LABEL_UPPERCASE = "Label of {0} with {1} ''''{2}'''' does not have uppercase first character for each word: ''''{3}''''";
76+
public static final String MESSAGE_MAX_LABEL_LENGTH = "Label of {0} with {1} ''''{2}'''' exceeds maximum length of %d characters with length {4}: ''''{3}''''";
77+
78+
public static final Set<String> LOWER_CASE_WORDS = Stream
79+
.of("a", "an", "the", "and", "as", "but", "by", "for", "from", "in", "into", "like", "near", "nor", "of",
80+
"onto", "or", "out", "over", "past", "so", "till", "to", "up", "upon", "with", "yet")
81+
.collect(Collectors.toSet());
82+
83+
private int maxLabelLength = Integer.MAX_VALUE;
84+
private int maxLabelLengthError = Integer.MAX_VALUE;
85+
private boolean doCheckWordCasing;
86+
private String dynamicMessageMaxLabelLength = "";
87+
private String dynamicMessageMaxLabelLengthError = "";
88+
89+
/**
90+
* Sets the configuration property for the max label length.
91+
*
92+
* @param maxLabelLength max length a label may have
93+
*/
94+
public void setMaxLabelLength(final String maxLabelLength) {
95+
this.maxLabelLength = maxLabelLength == null ? Integer.MAX_VALUE : Integer.parseInt(maxLabelLength);
96+
dynamicMessageMaxLabelLength = String.format(MESSAGE_MAX_LABEL_LENGTH, this.maxLabelLength);
97+
}
98+
99+
/**
100+
* Sets the configuration property for the max label length.
101+
*
102+
* @param maxLabelLength max length a label may have
103+
*/
104+
public void setMaxLabelLengthError(final String maxLabelLength) {
105+
this.maxLabelLengthError = maxLabelLength == null ? Integer.MAX_VALUE : Integer.parseInt(maxLabelLength);
106+
dynamicMessageMaxLabelLengthError = String.format(MESSAGE_MAX_LABEL_LENGTH, this.maxLabelLengthError);
107+
}
108+
109+
public void setCheckWordCasing(final String check) {
110+
this.doCheckWordCasing = Boolean.valueOf(check);
111+
}
112+
113+
@Override
114+
protected void checkConfigFile(final FileText xmlFileText) throws CheckstyleException {
115+
for (final String expression : ALL_CONFIG_EXPRESSIONS) {
116+
evaluateExpressionOnFile(xmlFileText, expression, CONFIG_ATTR_KEY);
117+
}
118+
}
119+
120+
@Override
121+
protected void checkBindingFile(final FileText xmlFileText) throws CheckstyleException {
122+
// No labels in binding files.
123+
}
124+
125+
@Override
126+
protected void checkThingTypeFile(final FileText xmlFileText) throws CheckstyleException {
127+
for (final String expression : ALL_THING_EXPRESSIONS) {
128+
evaluateExpressionOnFile(xmlFileText, expression, THING_ATTR_KEY);
129+
}
130+
for (final String expression : ALL_THING_CONFIG_EXPRESSIONS) {
131+
evaluateExpressionOnFile(xmlFileText, expression, CONFIG_ATTR_KEY);
132+
}
133+
}
134+
135+
private void evaluateExpressionOnFile(final FileText xmlFileText, final String xPathExpression, final String key)
136+
throws CheckstyleException {
137+
final String type = fiterType(xPathExpression);
138+
final Map<String, Integer> lineNumberMap = new HashMap<>();
139+
final NodeList nodes = getNodes(xmlFileText, xPathExpression);
140+
final File file = xmlFileText.getFile();
141+
142+
if (nodes != null) {
143+
for (int i = 0; i < nodes.getLength(); i++) {
144+
final String labelText = nodes.item(i).getNodeValue();
145+
if (noI18NLabel(labelText)) {
146+
checkWordCasing(xmlFileText, lineNumberMap, key, type, nodes.item(i), file, labelText);
147+
checkLabelLength(xmlFileText, lineNumberMap, key, type, nodes.item(i), file, labelText);
148+
}
149+
}
150+
}
151+
}
152+
153+
/**
154+
* Check if the label is an i18n key. If such it should not be checked for length.
155+
*
156+
* @param labelText label text to check
157+
* @return true if no i18n label
158+
*/
159+
private boolean noI18NLabel(String labelText) {
160+
return !labelText.startsWith(I18N_PREFIX);
161+
}
162+
163+
private void checkWordCasing(final FileText xmlFileText, final Map<String, Integer> lineNumberMap, final String key,
164+
final String type, final Node node, final File file, final String labelText) {
165+
if (!doCheckWordCasing) {
166+
return;
167+
}
168+
final String[] words = labelText.split("\\s+");
169+
170+
for (int i = 0; i < words.length; i++) {
171+
final String word = words[i];
172+
173+
final int firtCharType = Character.getType(word.charAt(0));
174+
final boolean lowerCase = firtCharType == Character.LOWERCASE_LETTER;
175+
final boolean firstOrLastWord = i == 0 || i == words.length - 1;
176+
boolean log = false;
177+
178+
if (lowerCase) {
179+
if ((firstOrLastWord || !LOWER_CASE_WORDS.contains(word)) && !SKIP_PATTERN.matcher(word).find()) {
180+
log = true;
181+
}
182+
} else if (!firstOrLastWord && LOWER_CASE_WORDS.contains(word)
183+
&& firtCharType == Character.UPPERCASE_LETTER) {
184+
log = true;
185+
}
186+
if (log) {
187+
log(xmlFileText, lineNumberMap, MESSAGE_LABEL_UPPERCASE, key, type, node, file, labelText);
188+
return;
189+
}
190+
}
191+
}
192+
193+
private void checkLabelLength(final FileText xmlFileText, final Map<String, Integer> lineNumberMap,
194+
final String key, final String type, final Node node, final File file, final String labelText) {
195+
if (labelText.length() > maxLabelLengthError) {
196+
final SeverityLevel configuredSeverityLevel = getSeverityLevel();
197+
setSeverity(SeverityLevel.ERROR.name());
198+
log(xmlFileText, lineNumberMap, dynamicMessageMaxLabelLengthError, key, type, node, file, labelText);
199+
setSeverity(configuredSeverityLevel.name());
200+
} else if (labelText.length() > maxLabelLength) {
201+
log(xmlFileText, lineNumberMap, dynamicMessageMaxLabelLength, key, type, node, file, labelText);
202+
}
203+
}
204+
205+
private void log(final FileText xmlFileText, final Map<String, Integer> lineNumberMap, final String message,
206+
final String key, final String type, final Node node, final File file, final String labelText) {
207+
lazyLoadMap(xmlFileText, lineNumberMap);
208+
final Integer lineNr = lineNumberMap.get(labelText);
209+
210+
logMessage(file.getPath(), lineNr == null ? 0 : lineNr, file.getName(),
211+
MessageFormat.format(message, type, key, getReferenceId(key, node), labelText, labelText.length()));
212+
}
213+
214+
private void lazyLoadMap(final FileText xmlFileText, final Map<String, Integer> lineNumberMap) {
215+
if (lineNumberMap.isEmpty()) {
216+
final String[] lines = xmlFileText.getFullText().toString().split(System.lineSeparator());
217+
for (int i = 0; i < lines.length; i++) {
218+
final Matcher matcher = LABEL_PATTERN.matcher(lines[i]);
219+
220+
if (matcher.find()) {
221+
lineNumberMap.put(matcher.group(1), i + 1);
222+
}
223+
}
224+
}
225+
}
226+
227+
private String getReferenceId(final String key, final Node node) {
228+
return node.getParentNode().getParentNode().getAttributes().getNamedItem(key).getNodeValue();
229+
}
230+
231+
private String fiterType(final String expression) {
232+
final Matcher matcher = TYPE_PATTERN.matcher(expression);
233+
return matcher.find() ? matcher.group(1) : "";
234+
}
235+
}

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

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,9 @@
1717
import java.util.HashMap;
1818
import java.util.Map;
1919

20-
import javax.xml.xpath.XPathConstants;
21-
import javax.xml.xpath.XPathExpression;
22-
import javax.xml.xpath.XPathExpressionException;
23-
2420
import org.openhab.tools.analysis.checkstyle.api.AbstractOhInfXmlCheck;
2521
import org.slf4j.Logger;
2622
import org.slf4j.LoggerFactory;
27-
import org.w3c.dom.Document;
2823
import org.w3c.dom.NodeList;
2924

3025
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
@@ -49,46 +44,46 @@ public class OhInfXmlUsageCheck extends AbstractOhInfXmlCheck {
4944

5045
private final Logger logger = LoggerFactory.getLogger(OhInfXmlUsageCheck.class);
5146

52-
private Map<String, File> allConfigDescriptionRefs = new HashMap<>();
53-
private Map<String, File> allConfigDescriptions = new HashMap<>();
47+
private final Map<String, File> allConfigDescriptionRefs = new HashMap<>();
48+
private final Map<String, File> allConfigDescriptions = new HashMap<>();
5449

55-
private Map<String, File> allSupportedBridges = new HashMap<>();
56-
private Map<String, File> allBridgeTypes = new HashMap<>();
50+
private final Map<String, File> allSupportedBridges = new HashMap<>();
51+
private final Map<String, File> allBridgeTypes = new HashMap<>();
5752

5853
@Override
5954
public void finishProcessing() {
6055
// Check for missing supported bridge-type-refs.
61-
Map<String, File> missingSupportedBridges = removeAll(allSupportedBridges, allBridgeTypes);
56+
final Map<String, File> missingSupportedBridges = removeAll(allSupportedBridges, allBridgeTypes);
6257
logMissingEntries(missingSupportedBridges, MESSAGE_MISSING_SUPPORTED_BRIDGE);
6358

6459
// Check for missing referenced config descriptions
65-
Map<String, File> missingConfigDescriptions = removeAll(allConfigDescriptionRefs, allConfigDescriptions);
60+
final Map<String, File> missingConfigDescriptions = removeAll(allConfigDescriptionRefs, allConfigDescriptions);
6661
logMissingEntries(missingConfigDescriptions, MESSAGE_MISSING_URI_CONFIGURATION);
6762

6863
// Check for unused bridge-type-refs.
69-
Map<String, File> unusedBridges = removeAll(allBridgeTypes, allSupportedBridges);
64+
final Map<String, File> unusedBridges = removeAll(allBridgeTypes, allSupportedBridges);
7065
logMissingEntries(unusedBridges, MESSAGE_UNUSED_BRIDGE);
7166

7267
// Check for unused referenced config descriptions
73-
Map<String, File> unusedConfigDescriptions = removeAll(allConfigDescriptions, allConfigDescriptionRefs);
68+
final Map<String, File> unusedConfigDescriptions = removeAll(allConfigDescriptions, allConfigDescriptionRefs);
7469
logMissingEntries(unusedConfigDescriptions, MESSAGE_UNUSED_URI_CONFIGURATION);
7570
}
7671

7772
@Override
78-
protected void checkConfigFile(FileText xmlFileText) throws CheckstyleException {
73+
protected void checkConfigFile(final FileText xmlFileText) throws CheckstyleException {
7974
// The allowed values are described in the config description XSD
8075
allConfigDescriptions.putAll(evaluateExpressionOnFile(xmlFileText, CONFIG_DESCRIPTION_EXPRESSION));
8176
}
8277

8378
@Override
84-
protected void checkBindingFile(FileText xmlFileText) throws CheckstyleException {
79+
protected void checkBindingFile(final FileText xmlFileText) throws CheckstyleException {
8580
// The allowed values are described in the binding XSD
8681
allConfigDescriptionRefs.putAll(evaluateExpressionOnFile(xmlFileText, CONFIG_DESCRIPTION_REF_EXPRESSION));
8782
allConfigDescriptions.putAll(evaluateExpressionOnFile(xmlFileText, CONFIG_DESCRIPTION_EXPRESSION));
8883
}
8984

9085
@Override
91-
protected void checkThingTypeFile(FileText xmlFileText) throws CheckstyleException {
86+
protected void checkThingTypeFile(final FileText xmlFileText) throws CheckstyleException {
9287
// Process the files for all nodes below,
9388
// the allowed values are described in the thing description XSD
9489
allSupportedBridges.putAll(evaluateExpressionOnFile(xmlFileText, SUPPORTED_BRIDGE_TYPE_REF_EXPRESSION));
@@ -97,10 +92,10 @@ protected void checkThingTypeFile(FileText xmlFileText) throws CheckstyleExcepti
9792
allConfigDescriptions.putAll(evaluateExpressionOnFile(xmlFileText, CONFIG_DESCRIPTION_EXPRESSION));
9893
}
9994

100-
private Map<String, File> evaluateExpressionOnFile(FileText xmlFileText, String xPathExpression)
95+
private Map<String, File> evaluateExpressionOnFile(final FileText xmlFileText, final String xPathExpression)
10196
throws CheckstyleException {
102-
Map<String, File> collection = new HashMap<>();
103-
NodeList nodes = getNodes(xmlFileText, xPathExpression);
97+
final Map<String, File> collection = new HashMap<>();
98+
final NodeList nodes = getNodes(xmlFileText, xPathExpression);
10499

105100
if (nodes != null) {
106101
for (int i = 0; i < nodes.getLength(); i++) {
@@ -110,30 +105,15 @@ private Map<String, File> evaluateExpressionOnFile(FileText xmlFileText, String
110105
return collection;
111106
}
112107

113-
private NodeList getNodes(FileText xmlFileText, String expression) throws CheckstyleException {
114-
Document document = parseDomDocumentFromFile(xmlFileText);
115-
116-
XPathExpression xpathExpression = compileXPathExpression(expression);
117-
118-
NodeList nodes = null;
119-
try {
120-
nodes = (NodeList) xpathExpression.evaluate(document, XPathConstants.NODESET);
121-
} catch (XPathExpressionException e) {
122-
logger.error("Problem occurred while evaluating the expression {} on the {} file.", expression,
123-
xmlFileText.getFile().getName(), e);
124-
}
125-
return nodes;
126-
}
127-
128-
private <K, V> Map<K, V> removeAll(Map<K, V> firstMap, Map<K, V> secondMap) {
129-
Map<K, V> result = new HashMap<>(firstMap);
108+
private <K, V> Map<K, V> removeAll(final Map<K, V> firstMap, final Map<K, V> secondMap) {
109+
final Map<K, V> result = new HashMap<>(firstMap);
130110
result.keySet().removeAll(secondMap.keySet());
131111
return result;
132112
}
133113

134-
private <K> void logMissingEntries(Map<K, File> collection, String message) {
135-
for (K element : collection.keySet()) {
136-
File xmlFile = collection.get(element);
114+
private <K> void logMissingEntries(final Map<K, File> collection, final String message) {
115+
for (final K element : collection.keySet()) {
116+
final File xmlFile = collection.get(element);
137117
logMessage(xmlFile.getPath(), 0, xmlFile.getName(), MessageFormat.format(message, element));
138118
}
139119
}

0 commit comments

Comments
 (0)