View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.internal.utils;
21  
22  import java.io.IOException;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  import java.util.regex.Matcher;
31  import java.util.regex.Pattern;
32  import java.util.stream.Collectors;
33  import java.util.stream.Stream;
34  
35  import javax.xml.parsers.DocumentBuilder;
36  import javax.xml.parsers.DocumentBuilderFactory;
37  import javax.xml.parsers.ParserConfigurationException;
38  
39  import org.w3c.dom.Document;
40  import org.w3c.dom.Element;
41  import org.w3c.dom.Node;
42  import org.w3c.dom.NodeList;
43  import org.xml.sax.SAXException;
44  
45  /**
46   * XdocUtil.
47   *
48   * @noinspection ClassOnlyUsedInOnePackage
49   * @noinspectionreason ClassOnlyUsedInOnePackage - class is internal tool, and only used in testing
50   */
51  public final class XdocUtil {
52  
53      public static final String DIRECTORY_PATH = "src/site/xdoc";
54  
55      private XdocUtil() {
56      }
57  
58      /**
59       * Gets xdocs file paths.
60       *
61       * @return a set of xdocs file paths.
62       * @throws IOException if an I/O error occurs.
63       */
64      public static Set<Path> getXdocsFilePaths() throws IOException {
65          final Path directory = Path.of(DIRECTORY_PATH);
66          try (Stream<Path> stream = Files.find(directory, Integer.MAX_VALUE,
67                  (path, attr) -> {
68                      return attr.isRegularFile()
69                              && (path.toString().endsWith(".xml")
70                              || path.toString().endsWith(".xml.vm"));
71                  })) {
72              return stream.collect(Collectors.toUnmodifiableSet());
73          }
74      }
75  
76      /**
77       * Gets xdocs template file paths. These are files ending with .xml.template.
78       * This module will be removed once
79       * <a href="https://github.com/checkstyle/checkstyle/issues/13426">#13426</a> is resolved.
80       *
81       * @return a set of xdocs template file paths.
82       * @throws IOException if an I/O error occurs.
83       */
84      public static Set<Path> getXdocsTemplatesFilePaths() throws IOException {
85          final Path directory = Path.of(DIRECTORY_PATH);
86          try (Stream<Path> stream = Files.find(directory, Integer.MAX_VALUE,
87                  (path, attr) -> {
88                      return attr.isRegularFile()
89                              && path.toString().endsWith(".xml.template");
90                  })) {
91              return stream.collect(Collectors.toUnmodifiableSet());
92          }
93      }
94  
95      /**
96       * Gets xdocs documentation file paths.
97       *
98       * @param files set of all xdoc files
99       * @return a set of xdocs config file paths.
100      */
101     public static Set<Path> getXdocsConfigFilePaths(Set<Path> files) {
102         final Set<Path> xdocs = new HashSet<>();
103         for (Path entry : files) {
104             final String fileName = entry.getFileName().toString();
105             if (!entry.getParent().toString().matches("src[\\\\/]site[\\\\/]xdocs")
106                     && fileName.endsWith(".xml")) {
107                 xdocs.add(entry);
108             }
109         }
110         return xdocs;
111     }
112 
113     /**
114      * Gets xdocs style file paths.
115      *
116      * @param files set of all xdoc files
117      * @return a set of xdocs style file paths.
118      */
119     public static Set<Path> getXdocsStyleFilePaths(Set<Path> files) {
120         final Set<Path> xdocs = new HashSet<>();
121         for (Path entry : files) {
122             final String fileName = entry.getFileName().toString();
123             if (fileName.endsWith("_style.xml")) {
124                 xdocs.add(entry);
125             }
126         }
127         return xdocs;
128     }
129 
130     /**
131      * Gets names of checkstyle's modules which are documented in xdocs.
132      *
133      * @return a set of checkstyle's modules which have xdoc documentation.
134      * @throws ParserConfigurationException if a DocumentBuilder cannot be created which satisfies
135      *              the configuration requested.
136      * @throws IOException if any IO errors occur.
137      * @throws SAXException if any parse errors occur.
138      */
139     public static Set<String> getModulesNamesWhichHaveXdoc() throws Exception {
140         final DocumentBuilderFactory factory = DocumentBuilderFactory
141                 .newInstance();
142 
143         // Validations of XML file make parsing too slow, that is why we disable
144         // all validations.
145         factory.setNamespaceAware(false);
146         factory.setValidating(false);
147         factory.setFeature("http://xml.org/sax/features/namespaces", false);
148         factory.setFeature("http://xml.org/sax/features/validation", false);
149         factory.setFeature(
150                 "http://apache.org/xml/features/nonvalidating/load-dtd-grammar",
151                 false);
152         factory.setFeature(
153                 "http://apache.org/xml/features/nonvalidating/load-external-dtd",
154                 false);
155 
156         final Set<String> modulesNamesWhichHaveXdoc = new HashSet<>();
157 
158         for (Path path : getXdocsConfigFilePaths(getXdocsFilePaths())) {
159             final DocumentBuilder builder = factory.newDocumentBuilder();
160             final Document document = builder.parse(path.toFile());
161 
162             // optional, but recommended
163             // FYI:
164             // http://stackoverflow.com/questions/13786607/normalization-in-dom-parsing-with-
165             // java-how-does-it-work
166             document.getDocumentElement().normalize();
167 
168             final NodeList nodeList = document.getElementsByTagName("section");
169 
170             for (int i = 0; i < nodeList.getLength(); i++) {
171                 final Node currentNode = nodeList.item(i);
172                 if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
173                     final Element module = (Element) currentNode;
174                     final String moduleName = module.getAttribute("name");
175                     if (!"Content".equals(moduleName)
176                             && !"Overview".equals(moduleName)) {
177                         modulesNamesWhichHaveXdoc.add(moduleName);
178                     }
179                 }
180             }
181         }
182         return modulesNamesWhichHaveXdoc;
183     }
184 
185     /**
186      * Extracts used properties from XDocs examples (from /*xml blocks).
187      *
188      * @return a map of Check name -> Set of used property names.
189      * @throws IOException if file I/O fails.
190      */
191     public static Map<String, Set<String>> extractUsedPropertiesFromXdocsExamples()
192             throws IOException {
193         final List<Path> roots = List.of(
194                 Path.of("src/xdocs-examples/resources/com/puppycrawl/tools/checkstyle/checks"),
195                 Path.of("src/xdocs-examples/resources-noncompilable/"
196                         + "com/puppycrawl/tools/checkstyle/checks")
197         );
198 
199         final Map<String, Set<String>> checkToProperties = new HashMap<>();
200 
201         for (Path root : roots) {
202             if (Files.exists(root)) {
203                 try (Stream<Path> paths = Files.walk(root)) {
204                     paths.filter(path -> path.toString().endsWith(".java"))
205                             .forEach(path -> processXdocExampleFile(path, checkToProperties));
206                 }
207             }
208         }
209 
210         return checkToProperties;
211     }
212 
213     private static void processXdocExampleFile(
214             Path file, Map<String, Set<String>> checkToProperties) {
215         try {
216             final String content = Files.readString(file);
217             final Matcher xmlBlockMatcher =
218                 Pattern.compile("/\\*xml(.*?)\\*/", Pattern.DOTALL).matcher(content);
219 
220             if (xmlBlockMatcher.find()) {
221                 final Map.Entry<String, Set<String>> entry =
222                     parseConfigBlock(xmlBlockMatcher.group(1));
223 
224                 if (entry != null) {
225                     checkToProperties
226                         .computeIfAbsent(entry.getKey(), key -> new HashSet<>())
227                         .addAll(entry.getValue());
228                 }
229             }
230         }
231         catch (IOException ioe) {
232             throw new IllegalStateException("Error reading file: " + file, ioe);
233         }
234     }
235 
236     private static Map.Entry<String, Set<String>> parseConfigBlock(String configBlock) {
237         final Matcher moduleMatcher =
238                 Pattern.compile("<module name=\"([^\"]+)\"").matcher(configBlock);
239         String lastModule = null;
240         while (moduleMatcher.find()) {
241             lastModule = moduleMatcher.group(1);
242         }
243 
244         final Matcher propMatcher =
245                 Pattern.compile("<property name=\"([^\"]+)\"").matcher(configBlock);
246         final Set<String> props = new HashSet<>();
247         while (propMatcher.find()) {
248             props.add(propMatcher.group(1));
249         }
250 
251         Map.Entry<String, Set<String>> result = null;
252         if (lastModule != null && !props.isEmpty()) {
253             final String checkClassName;
254             if (lastModule.endsWith("Check")) {
255                 checkClassName = lastModule;
256             }
257             else {
258                 checkClassName = lastModule + "Check";
259             }
260             result = Map.entry(checkClassName, props);
261         }
262 
263         return result;
264     }
265 }