View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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.checks.imports;
21  
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.net.MalformedURLException;
25  import java.net.URI;
26  import java.util.ArrayDeque;
27  import java.util.Deque;
28  import java.util.HashMap;
29  import java.util.Map;
30  
31  import javax.xml.parsers.ParserConfigurationException;
32  
33  import org.xml.sax.Attributes;
34  import org.xml.sax.InputSource;
35  import org.xml.sax.SAXException;
36  
37  import com.puppycrawl.tools.checkstyle.XmlLoader;
38  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
39  
40  /**
41   * Responsible for loading the contents of an import control configuration file.
42   */
43  public final class ImportControlLoader extends XmlLoader {
44  
45      /** The public ID for the configuration dtd. */
46      private static final String DTD_PUBLIC_ID_1_0 =
47          "-//Puppy Crawl//DTD Import Control 1.0//EN";
48  
49      /** The new public ID for version 1_0 of the configuration dtd. */
50      private static final String DTD_PUBLIC_CS_ID_1_0 =
51          "-//Checkstyle//DTD ImportControl Configuration 1.0//EN";
52  
53      /** The public ID for the configuration dtd. */
54      private static final String DTD_PUBLIC_ID_1_1 =
55          "-//Puppy Crawl//DTD Import Control 1.1//EN";
56  
57      /** The new public ID for version 1_1 of the configuration dtd. */
58      private static final String DTD_PUBLIC_CS_ID_1_1 =
59          "-//Checkstyle//DTD ImportControl Configuration 1.1//EN";
60  
61      /** The public ID for the configuration dtd. */
62      private static final String DTD_PUBLIC_ID_1_2 =
63          "-//Puppy Crawl//DTD Import Control 1.2//EN";
64  
65      /** The new public ID for version 1_2 of the configuration dtd. */
66      private static final String DTD_PUBLIC_CS_ID_1_2 =
67          "-//Checkstyle//DTD ImportControl Configuration 1.2//EN";
68  
69      /** The public ID for the configuration dtd. */
70      private static final String DTD_PUBLIC_ID_1_3 =
71          "-//Puppy Crawl//DTD Import Control 1.3//EN";
72  
73      /** The new public ID for version 1_3 of the configuration dtd. */
74      private static final String DTD_PUBLIC_CS_ID_1_3 =
75          "-//Checkstyle//DTD ImportControl Configuration 1.3//EN";
76  
77      /** The public ID for the configuration dtd. */
78      private static final String DTD_PUBLIC_ID_1_4 =
79          "-//Puppy Crawl//DTD Import Control 1.4//EN";
80  
81      /** The new public ID for version 1_4 of the configuration dtd. */
82      private static final String DTD_PUBLIC_CS_ID_1_4 =
83          "-//Checkstyle//DTD ImportControl Configuration 1.4//EN";
84  
85      /** The public ID for the configuration dtd. */
86      private static final String DTD_PUBLIC_ID_1_5 =
87          "-//Puppy Crawl//DTD Import Control 1.5//EN";
88  
89      /** The new public ID for version 1_5 of the configuration dtd. */
90      private static final String DTD_PUBLIC_CS_ID_1_5 =
91          "-//Checkstyle//DTD ImportControl Configuration 1.5//EN";
92  
93      /** The resource for the configuration dtd. */
94      private static final String DTD_RESOURCE_NAME_1_0 =
95          "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_0.dtd";
96  
97      /** The resource for the configuration dtd. */
98      private static final String DTD_RESOURCE_NAME_1_1 =
99          "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_1.dtd";
100 
101     /** The resource for the configuration dtd. */
102     private static final String DTD_RESOURCE_NAME_1_2 =
103         "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_2.dtd";
104 
105     /** The resource for the configuration dtd. */
106     private static final String DTD_RESOURCE_NAME_1_3 =
107         "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_3.dtd";
108 
109     /** The resource for the configuration dtd. */
110     private static final String DTD_RESOURCE_NAME_1_4 =
111         "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_4.dtd";
112 
113     /** The resource for the configuration dtd. */
114     private static final String DTD_RESOURCE_NAME_1_5 =
115         "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_5.dtd";
116 
117     /** The map to look up the resource name by the id. */
118     private static final Map<String, String> DTD_RESOURCE_BY_ID = new HashMap<>();
119 
120     /** Name for attribute 'pkg'. */
121     private static final String PKG_ATTRIBUTE_NAME = "pkg";
122 
123     /** Name for attribute 'name'. */
124     private static final String NAME_ATTRIBUTE_NAME = "name";
125 
126     /** Name for attribute 'strategyOnMismatch'. */
127     private static final String STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME = "strategyOnMismatch";
128 
129     /** Value "allowed" for attribute 'strategyOnMismatch'. */
130     private static final String STRATEGY_ON_MISMATCH_ALLOWED_VALUE = "allowed";
131 
132     /** Value "disallowed" for attribute 'strategyOnMismatch'. */
133     private static final String STRATEGY_ON_MISMATCH_DISALLOWED_VALUE = "disallowed";
134 
135     /** Qualified name for element 'subpackage'. */
136     private static final String SUBPACKAGE_ELEMENT_NAME = "subpackage";
137 
138     /** Qualified name for element 'file'. */
139     private static final String FILE_ELEMENT_NAME = "file";
140 
141     /** Qualified name for element 'allow'. */
142     private static final String ALLOW_ELEMENT_NAME = "allow";
143 
144     /** Used to hold the {@link AbstractImportControl} objects. */
145     private final Deque<AbstractImportControl> stack = new ArrayDeque<>();
146 
147     static {
148         DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_0, DTD_RESOURCE_NAME_1_0);
149         DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_1, DTD_RESOURCE_NAME_1_1);
150         DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_2, DTD_RESOURCE_NAME_1_2);
151         DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_3, DTD_RESOURCE_NAME_1_3);
152         DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_4, DTD_RESOURCE_NAME_1_4);
153         DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_5, DTD_RESOURCE_NAME_1_5);
154         DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_CS_ID_1_0, DTD_RESOURCE_NAME_1_0);
155         DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_CS_ID_1_1, DTD_RESOURCE_NAME_1_1);
156         DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_CS_ID_1_2, DTD_RESOURCE_NAME_1_2);
157         DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_CS_ID_1_3, DTD_RESOURCE_NAME_1_3);
158         DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_CS_ID_1_4, DTD_RESOURCE_NAME_1_4);
159         DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_CS_ID_1_5, DTD_RESOURCE_NAME_1_5);
160     }
161 
162     /**
163      * Constructs an instance.
164      *
165      * @throws ParserConfigurationException if an error occurs.
166      * @throws SAXException if an error occurs.
167      */
168     private ImportControlLoader() throws ParserConfigurationException,
169             SAXException {
170         super(DTD_RESOURCE_BY_ID);
171     }
172 
173     @Override
174     public void startElement(String namespaceUri,
175                              String localName,
176                              String qName,
177                              Attributes attributes)
178             throws SAXException {
179         if ("import-control".equals(qName)) {
180             final String pkg = safeGet(attributes, PKG_ATTRIBUTE_NAME);
181             final MismatchStrategy strategyOnMismatch = getStrategyForImportControl(attributes);
182             final boolean regex = containsRegexAttribute(attributes);
183             stack.push(new PkgImportControl(pkg, regex, strategyOnMismatch));
184         }
185         else if (SUBPACKAGE_ELEMENT_NAME.equals(qName)) {
186             final String name = safeGet(attributes, NAME_ATTRIBUTE_NAME);
187             final MismatchStrategy strategyOnMismatch = getStrategyForSubpackage(attributes);
188             final boolean regex = containsRegexAttribute(attributes);
189             final PkgImportControl parentImportControl = (PkgImportControl) stack.peek();
190             final AbstractImportControl importControl = new PkgImportControl(parentImportControl,
191                     name, regex, strategyOnMismatch);
192             parentImportControl.addChild(importControl);
193             stack.push(importControl);
194         }
195         else if (FILE_ELEMENT_NAME.equals(qName)) {
196             final String name = safeGet(attributes, NAME_ATTRIBUTE_NAME);
197             final boolean regex = containsRegexAttribute(attributes);
198             final PkgImportControl parentImportControl = (PkgImportControl) stack.peek();
199             final AbstractImportControl importControl = new FileImportControl(parentImportControl,
200                     name, regex);
201             parentImportControl.addChild(importControl);
202             stack.push(importControl);
203         }
204         else {
205             final AbstractImportRule rule = createImportRule(qName, attributes);
206             stack.peek().addImportRule(rule);
207         }
208     }
209 
210     /**
211      * Constructs an instance of an import rule based on the given {@code name} and
212      * {@code attributes}.
213      *
214      * @param qName The qualified name.
215      * @param attributes The attributes attached to the element.
216      * @return The created import rule.
217      * @throws SAXException if an error occurs.
218      */
219     private static AbstractImportRule createImportRule(String qName, Attributes attributes)
220             throws SAXException {
221 
222         final boolean isAllow = ALLOW_ELEMENT_NAME.equals(qName);
223         final boolean isLocalOnly = attributes.getValue("local-only") != null;
224         final String pkg = attributes.getValue(PKG_ATTRIBUTE_NAME);
225         final String module = attributes.getValue("module");
226         final boolean regex = containsRegexAttribute(attributes);
227         final AbstractImportRule rule;
228 
229         if (module != null) {
230             rule = new ModuleImportRule(isAllow, isLocalOnly, module, regex);
231         }
232         else if (pkg != null) {
233             final boolean exactMatch = attributes.getValue("exact-match") != null;
234             rule = new PkgImportRule(isAllow, isLocalOnly, pkg, exactMatch, regex);
235         }
236         else {
237             final String clazz = safeGet(attributes, "class");
238             rule = new ClassImportRule(isAllow, isLocalOnly, clazz, regex);
239         }
240         return rule;
241     }
242 
243     /**
244      * Check if the given attributes contain the regex attribute.
245      *
246      * @param attributes the attributes.
247      * @return if the regex attribute is contained.
248      */
249     private static boolean containsRegexAttribute(Attributes attributes) {
250         return attributes.getValue("regex") != null;
251     }
252 
253     @Override
254     public void endElement(String namespaceUri, String localName,
255         String qName) {
256         if (SUBPACKAGE_ELEMENT_NAME.equals(qName) || FILE_ELEMENT_NAME.equals(qName)) {
257             stack.pop();
258         }
259     }
260 
261     /**
262      * Loads the import control file from a file.
263      *
264      * @param uri the uri of the file to load.
265      * @return the root {@link PkgImportControl} object.
266      * @throws CheckstyleException if an error occurs.
267      */
268     public static PkgImportControl load(URI uri) throws CheckstyleException {
269         return loadUri(uri);
270     }
271 
272     /**
273      * Loads the import control file from a {@link InputSource}.
274      *
275      * @param source the source to load from.
276      * @param uri uri of the source being loaded.
277      * @return the root {@link PkgImportControl} object.
278      * @throws CheckstyleException if an error occurs.
279      */
280     private static PkgImportControl load(InputSource source,
281         URI uri) throws CheckstyleException {
282         try {
283             final ImportControlLoader loader = new ImportControlLoader();
284             loader.parseInputSource(source);
285             return loader.getRoot();
286         }
287         catch (ParserConfigurationException | SAXException exc) {
288             throw new CheckstyleException("unable to parse " + uri, exc);
289         }
290         catch (IOException exc) {
291             throw new CheckstyleException("unable to read " + uri, exc);
292         }
293     }
294 
295     /**
296      * Loads the import control file from a URI.
297      *
298      * @param uri the uri of the file to load.
299      * @return the root {@link PkgImportControl} object.
300      * @throws CheckstyleException if an error occurs.
301      */
302     private static PkgImportControl loadUri(URI uri) throws CheckstyleException {
303         try (InputStream inputStream = uri.toURL().openStream()) {
304             final InputSource source = new InputSource(inputStream);
305             return load(source, uri);
306         }
307         catch (MalformedURLException exc) {
308             throw new CheckstyleException("syntax error in url " + uri, exc);
309         }
310         catch (IOException exc) {
311             throw new CheckstyleException("unable to find " + uri, exc);
312         }
313     }
314 
315     /**
316      * Returns root PkgImportControl.
317      *
318      * @return the root {@link PkgImportControl} object loaded.
319      */
320     private PkgImportControl getRoot() {
321         return (PkgImportControl) stack.peek();
322     }
323 
324     /**
325      * Utility to get a strategyOnMismatch property for "import-control" tag.
326      *
327      * @param attributes collect to get attribute from.
328      * @return the value of the attribute.
329      */
330     private static MismatchStrategy getStrategyForImportControl(Attributes attributes) {
331         final String returnValue = attributes.getValue(STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME);
332         MismatchStrategy strategyOnMismatch = MismatchStrategy.DISALLOWED;
333         if (STRATEGY_ON_MISMATCH_ALLOWED_VALUE.equals(returnValue)) {
334             strategyOnMismatch = MismatchStrategy.ALLOWED;
335         }
336         return strategyOnMismatch;
337     }
338 
339     /**
340      * Utility to get a strategyOnMismatch property for "subpackage" tag.
341      *
342      * @param attributes collect to get attribute from.
343      * @return the value of the attribute.
344      */
345     private static MismatchStrategy getStrategyForSubpackage(Attributes attributes) {
346         final String returnValue = attributes.getValue(STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME);
347         MismatchStrategy strategyOnMismatch = MismatchStrategy.DELEGATE_TO_PARENT;
348         if (STRATEGY_ON_MISMATCH_ALLOWED_VALUE.equals(returnValue)) {
349             strategyOnMismatch = MismatchStrategy.ALLOWED;
350         }
351         else if (STRATEGY_ON_MISMATCH_DISALLOWED_VALUE.equals(returnValue)) {
352             strategyOnMismatch = MismatchStrategy.DISALLOWED;
353         }
354         return strategyOnMismatch;
355     }
356 
357     /**
358      * Utility to safely get an attribute. If it does not exist an exception
359      * is thrown.
360      *
361      * @param attributes collect to get attribute from.
362      * @param name name of the attribute to get.
363      * @return the value of the attribute.
364      * @throws SAXException if the attribute does not exist.
365      */
366     private static String safeGet(Attributes attributes, String name)
367             throws SAXException {
368         final String returnValue = attributes.getValue(name);
369         if (returnValue == null) {
370             // -@cs[IllegalInstantiation] SAXException is in the overridden method signature
371             // of the only method which calls the current one
372             throw new SAXException("missing attribute " + name);
373         }
374         return returnValue;
375     }
376 
377 }