001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2021 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.meta;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.util.ArrayList;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Set;
028import java.util.regex.Pattern;
029
030import javax.xml.XMLConstants;
031import javax.xml.parsers.DocumentBuilder;
032import javax.xml.parsers.DocumentBuilderFactory;
033import javax.xml.parsers.ParserConfigurationException;
034
035import org.reflections.Reflections;
036import org.reflections.ReflectionsException;
037import org.reflections.scanners.ResourcesScanner;
038import org.w3c.dom.Document;
039import org.w3c.dom.Element;
040import org.w3c.dom.NodeList;
041import org.xml.sax.SAXException;
042
043/**
044 * Class having utilities required to read module details from an XML metadata file of a module.
045 * This class is used by plugins that need load of metadata from XML files.
046 */
047public final class XmlMetaReader {
048
049    /** Name tag of metadata XML files. */
050    private static final String XML_TAG_NAME = "name";
051
052    /** Description tag of metadata XML files. */
053    private static final String XML_TAG_DESCRIPTION = "description";
054
055    /**
056     * Do no allow {@code XmlMetaReader} instances to be created.
057     */
058    private XmlMetaReader() {
059    }
060
061    /**
062     * Utility to load all the metadata files present in the checkstyle JAR including third parties'
063     * module metadata files.
064     * checkstyle metadata files are grouped in a folder hierarchy similar to that of their
065     * corresponding source files.
066     * Third party(e.g. SevNTU Checks) metadata files are prefixed with {@code checkstylemeta-}
067     * to their file names.
068     *
069     * @param thirdPartyPackages list of fully qualified third party package names(can be only a
070     *                           hint, e.g. for SevNTU it can be com.github.sevntu / com.github)
071     * @return list of module details found in the classpath satisfying the above conditions
072     * @throws IllegalStateException if there was a problem reading the module metadata files
073     */
074    public static List<ModuleDetails> readAllModulesIncludingThirdPartyIfAny(
075            String... thirdPartyPackages) {
076        final Set<String> standardModuleFileNames =
077                new Reflections("com.puppycrawl.tools.checkstyle.meta",
078                        new ResourcesScanner()).getResources(Pattern.compile(".*\\.xml"));
079        final Set<String> allMetadataSources = new HashSet<>(standardModuleFileNames);
080        for (String packageName : thirdPartyPackages) {
081            try {
082                final Set<String> thirdPartyModuleFileNames =
083                        new Reflections(packageName, new ResourcesScanner())
084                                .getResources(Pattern.compile(".*checkstylemeta-.*\\.xml"));
085                allMetadataSources.addAll(thirdPartyModuleFileNames);
086            }
087            catch (ReflectionsException ex) {
088                if (propagateResourceScannerException(ex)) {
089                    throw ex;
090                }
091            }
092        }
093
094        final List<ModuleDetails> result = new ArrayList<>();
095        for (String fileName : allMetadataSources) {
096            final ModuleType moduleType;
097            if (fileName.endsWith("FileFilter.xml")) {
098                moduleType = ModuleType.FILEFILTER;
099            }
100            else if (fileName.endsWith("Filter.xml")) {
101                moduleType = ModuleType.FILTER;
102            }
103            else {
104                moduleType = ModuleType.CHECK;
105            }
106            final ModuleDetails moduleDetails;
107            try {
108                moduleDetails = read(XmlMetaReader.class.getResourceAsStream("/" + fileName),
109                        moduleType);
110            }
111            catch (ParserConfigurationException | IOException | SAXException ex) {
112                throw new IllegalStateException("Problem to read all modules including third "
113                        + "party if any. Problem detected at file: " + fileName, ex);
114            }
115            result.add(moduleDetails);
116        }
117
118        return result;
119    }
120
121    /**
122     * Checks if exception should be suppressed that matches the following
123     * because the reflections library throws this exception wrongly on valid
124     * packages with no resources to be found. Packages may be only checks with
125     * no resources or checks with resources. This issue is until
126     * https://github.com/ronmamo/reflections/issues/273.
127     *
128     * @param exception The exception to examine.
129     * @return {@code true} if the exception should be propagated.
130     */
131    private static boolean propagateResourceScannerException(ReflectionsException exception) {
132        return !"Scanner ResourcesScanner was not configured".equals(exception.getMessage());
133    }
134
135    /**
136     * Read the module details from the supplied input stream of the module's XML metadata file.
137     *
138     * @param moduleMetadataStream input stream object of a module's metadata file
139     * @param moduleType type of module
140     * @return module detail object extracted from the XML metadata file
141     * @throws ParserConfigurationException if a parser configuration exception occurs
142     * @throws IOException if a IO exception occurs
143     * @throws SAXException if a SAX exception occurs during parsing the XML file
144     */
145    public static ModuleDetails read(InputStream moduleMetadataStream, ModuleType moduleType)
146            throws ParserConfigurationException, IOException, SAXException {
147        ModuleDetails result = null;
148        if (moduleType != null) {
149            final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
150            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
151            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
152            final DocumentBuilder builder = factory.newDocumentBuilder();
153            final Document document = builder.parse(moduleMetadataStream);
154            final Element root = document.getDocumentElement();
155            final Element element = getDirectChildsByTag(root, "module").get(0);
156            final ModuleDetails moduleDetails = new ModuleDetails();
157            final Element module = getDirectChildsByTag(element, moduleType.getLabel()).get(0);
158            moduleDetails.setModuleType(moduleType);
159            result = createModule(module, moduleDetails);
160        }
161        return result;
162    }
163
164    /**
165     * Create the module detail object from XML metadata.
166     *
167     * @param mod root XML document element
168     * @param moduleDetails module detail object, which is to be updated
169     * @return module detail object containing all metadata
170     */
171    private static ModuleDetails createModule(Element mod, ModuleDetails moduleDetails) {
172        moduleDetails.setName(getAttributeValue(mod, XML_TAG_NAME));
173        moduleDetails.setFullQualifiedName(getAttributeValue(mod, "fully-qualified-name"));
174        moduleDetails.setParent(getAttributeValue(mod, "parent"));
175        moduleDetails.setDescription(getDirectChildsByTag(mod, XML_TAG_DESCRIPTION).get(0)
176                .getFirstChild().getNodeValue());
177        final List<Element> properties = getDirectChildsByTag(mod, "properties");
178        if (!properties.isEmpty()) {
179            final List<ModulePropertyDetails> modulePropertyDetailsList =
180                    createProperties(properties.get(0));
181            moduleDetails.addToProperties(modulePropertyDetailsList);
182        }
183        final List<String> messageKeys =
184                getListContentByAttribute(mod,
185                        "message-keys", "message-key", "key");
186        if (messageKeys != null) {
187            moduleDetails.addToViolationMessages(messageKeys);
188        }
189        return moduleDetails;
190    }
191
192    /**
193     * Create module property details from the XML metadata.
194     *
195     * @param properties parent document element which contains property's metadata
196     * @return list of property details object created
197     */
198    private static List<ModulePropertyDetails> createProperties(Element properties) {
199        final List<ModulePropertyDetails> result = new ArrayList<>();
200        final NodeList propertyList = properties.getElementsByTagName("property");
201        for (int i = 0; i < propertyList.getLength(); i++) {
202            final ModulePropertyDetails propertyDetails = new ModulePropertyDetails();
203            final Element prop = (Element) propertyList.item(i);
204            propertyDetails.setName(getAttributeValue(prop, XML_TAG_NAME));
205            propertyDetails.setType(getAttributeValue(prop, "type"));
206            final String defaultValueTag = "default-value";
207            if (prop.hasAttribute(defaultValueTag)) {
208                propertyDetails.setDefaultValue(getAttributeValue(prop, defaultValueTag));
209            }
210            final String validationTypeTag = "validation-type";
211            if (prop.hasAttribute(validationTypeTag)) {
212                propertyDetails.setValidationType(getAttributeValue(prop, validationTypeTag));
213            }
214            propertyDetails.setDescription(getDirectChildsByTag(prop, XML_TAG_DESCRIPTION)
215                    .get(0).getFirstChild().getNodeValue());
216            result.add(propertyDetails);
217        }
218        return result;
219    }
220
221    /**
222     * Utility to get the list contents by the attribute specified.
223     *
224     * @param element doc element
225     * @param listParent parent element of list
226     * @param listOption child list element
227     * @param attribute attribute key
228     * @return list of strings containing the XML list data
229     */
230    private static List<String> getListContentByAttribute(Element element, String listParent,
231                                                         String listOption, String attribute) {
232        final List<Element> children = getDirectChildsByTag(element, listParent);
233        List<String> result = null;
234        if (!children.isEmpty()) {
235            final NodeList nodeList = children.get(0).getElementsByTagName(listOption);
236            final List<String> listContent = new ArrayList<>();
237            for (int j = 0; j < nodeList.getLength(); j++) {
238                listContent.add(getAttributeValue((Element) nodeList.item(j), attribute));
239            }
240            result = listContent;
241        }
242        return result;
243    }
244
245    /**
246     * Utility to get the children of an element by tag name.
247     *
248     * @param element parent element
249     * @param sTagName tag name of children required
250     * @return list of elements retrieved
251     */
252    private static List<Element> getDirectChildsByTag(Element element, String sTagName) {
253        final NodeList children = element.getElementsByTagName(sTagName);
254        final List<Element> res = new ArrayList<>();
255        for (int i = 0; i < children.getLength(); i++) {
256            if (children.item(i).getParentNode().equals(element)) {
257                res.add((Element) children.item(i));
258            }
259        }
260        return res;
261    }
262
263    /**
264     * Utility to get attribute value of an element.
265     *
266     * @param element target element
267     * @param attribute attribute key
268     * @return attribute value
269     */
270    private static String getAttributeValue(Element element, String attribute) {
271        return element.getAttributes().getNamedItem(attribute).getNodeValue();
272    }
273
274}