001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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.checks.imports; 021 022import java.io.IOException; 023import java.io.InputStream; 024import java.net.MalformedURLException; 025import java.net.URI; 026import java.util.ArrayDeque; 027import java.util.Deque; 028import java.util.HashMap; 029import java.util.Map; 030 031import javax.xml.parsers.ParserConfigurationException; 032 033import org.xml.sax.Attributes; 034import org.xml.sax.InputSource; 035import org.xml.sax.SAXException; 036 037import com.puppycrawl.tools.checkstyle.XmlLoader; 038import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 039 040/** 041 * Responsible for loading the contents of an import control configuration file. 042 */ 043public final class ImportControlLoader extends XmlLoader { 044 045 /** The public ID for the configuration dtd. */ 046 private static final String DTD_PUBLIC_ID_1_0 = 047 "-//Puppy Crawl//DTD Import Control 1.0//EN"; 048 049 /** The new public ID for version 1_0 of the configuration dtd. */ 050 private static final String DTD_PUBLIC_CS_ID_1_0 = 051 "-//Checkstyle//DTD ImportControl Configuration 1.0//EN"; 052 053 /** The public ID for the configuration dtd. */ 054 private static final String DTD_PUBLIC_ID_1_1 = 055 "-//Puppy Crawl//DTD Import Control 1.1//EN"; 056 057 /** The new public ID for version 1_1 of the configuration dtd. */ 058 private static final String DTD_PUBLIC_CS_ID_1_1 = 059 "-//Checkstyle//DTD ImportControl Configuration 1.1//EN"; 060 061 /** The public ID for the configuration dtd. */ 062 private static final String DTD_PUBLIC_ID_1_2 = 063 "-//Puppy Crawl//DTD Import Control 1.2//EN"; 064 065 /** The new public ID for version 1_2 of the configuration dtd. */ 066 private static final String DTD_PUBLIC_CS_ID_1_2 = 067 "-//Checkstyle//DTD ImportControl Configuration 1.2//EN"; 068 069 /** The public ID for the configuration dtd. */ 070 private static final String DTD_PUBLIC_ID_1_3 = 071 "-//Puppy Crawl//DTD Import Control 1.3//EN"; 072 073 /** The new public ID for version 1_3 of the configuration dtd. */ 074 private static final String DTD_PUBLIC_CS_ID_1_3 = 075 "-//Checkstyle//DTD ImportControl Configuration 1.3//EN"; 076 077 /** The public ID for the configuration dtd. */ 078 private static final String DTD_PUBLIC_ID_1_4 = 079 "-//Puppy Crawl//DTD Import Control 1.4//EN"; 080 081 /** The new public ID for version 1_4 of the configuration dtd. */ 082 private static final String DTD_PUBLIC_CS_ID_1_4 = 083 "-//Checkstyle//DTD ImportControl Configuration 1.4//EN"; 084 085 /** The public ID for the configuration dtd. */ 086 private static final String DTD_PUBLIC_ID_1_5 = 087 "-//Puppy Crawl//DTD Import Control 1.5//EN"; 088 089 /** The new public ID for version 1_5 of the configuration dtd. */ 090 private static final String DTD_PUBLIC_CS_ID_1_5 = 091 "-//Checkstyle//DTD ImportControl Configuration 1.5//EN"; 092 093 /** The resource for the configuration dtd. */ 094 private static final String DTD_RESOURCE_NAME_1_0 = 095 "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_0.dtd"; 096 097 /** The resource for the configuration dtd. */ 098 private static final String DTD_RESOURCE_NAME_1_1 = 099 "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 // Need to handle either "pkg" or "class" attribute. 222 // May have "exact-match" for "pkg" 223 // May have "local-only" 224 final boolean isAllow = ALLOW_ELEMENT_NAME.equals(qName); 225 final boolean isLocalOnly = attributes.getValue("local-only") != null; 226 final String pkg = attributes.getValue(PKG_ATTRIBUTE_NAME); 227 final String module = attributes.getValue("module"); 228 final boolean regex = containsRegexAttribute(attributes); 229 final AbstractImportRule rule; 230 231 if (module != null) { 232 rule = new ModuleImportRule(isAllow, isLocalOnly, module, regex); 233 } 234 else if (pkg == null) { 235 // handle class names which can be normal class names or regular 236 // expressions 237 final String clazz = safeGet(attributes, "class"); 238 rule = new ClassImportRule(isAllow, isLocalOnly, clazz, regex); 239 } 240 else { 241 final boolean exactMatch = 242 attributes.getValue("exact-match") != null; 243 rule = new PkgImportRule(isAllow, isLocalOnly, pkg, exactMatch, regex); 244 } 245 return rule; 246 } 247 248 /** 249 * Check if the given attributes contain the regex attribute. 250 * 251 * @param attributes the attributes. 252 * @return if the regex attribute is contained. 253 */ 254 private static boolean containsRegexAttribute(Attributes attributes) { 255 return attributes.getValue("regex") != null; 256 } 257 258 @Override 259 public void endElement(String namespaceUri, String localName, 260 String qName) { 261 if (SUBPACKAGE_ELEMENT_NAME.equals(qName) || FILE_ELEMENT_NAME.equals(qName)) { 262 stack.pop(); 263 } 264 } 265 266 /** 267 * Loads the import control file from a file. 268 * 269 * @param uri the uri of the file to load. 270 * @return the root {@link PkgImportControl} object. 271 * @throws CheckstyleException if an error occurs. 272 */ 273 public static PkgImportControl load(URI uri) throws CheckstyleException { 274 return loadUri(uri); 275 } 276 277 /** 278 * Loads the import control file from a {@link InputSource}. 279 * 280 * @param source the source to load from. 281 * @param uri uri of the source being loaded. 282 * @return the root {@link PkgImportControl} object. 283 * @throws CheckstyleException if an error occurs. 284 */ 285 private static PkgImportControl load(InputSource source, 286 URI uri) throws CheckstyleException { 287 try { 288 final ImportControlLoader loader = new ImportControlLoader(); 289 loader.parseInputSource(source); 290 return loader.getRoot(); 291 } 292 catch (ParserConfigurationException | SAXException exc) { 293 throw new CheckstyleException("unable to parse " + uri 294 + " - " + exc.getMessage(), exc); 295 } 296 catch (IOException exc) { 297 throw new CheckstyleException("unable to read " + uri, exc); 298 } 299 } 300 301 /** 302 * Loads the import control file from a URI. 303 * 304 * @param uri the uri of the file to load. 305 * @return the root {@link PkgImportControl} object. 306 * @throws CheckstyleException if an error occurs. 307 */ 308 private static PkgImportControl loadUri(URI uri) throws CheckstyleException { 309 try (InputStream inputStream = uri.toURL().openStream()) { 310 final InputSource source = new InputSource(inputStream); 311 return load(source, uri); 312 } 313 catch (MalformedURLException exc) { 314 throw new CheckstyleException("syntax error in url " + uri, exc); 315 } 316 catch (IOException exc) { 317 throw new CheckstyleException("unable to find " + uri, exc); 318 } 319 } 320 321 /** 322 * Returns root PkgImportControl. 323 * 324 * @return the root {@link PkgImportControl} object loaded. 325 */ 326 private PkgImportControl getRoot() { 327 return (PkgImportControl) stack.peek(); 328 } 329 330 /** 331 * Utility to get a strategyOnMismatch property for "import-control" tag. 332 * 333 * @param attributes collect to get attribute from. 334 * @return the value of the attribute. 335 */ 336 private static MismatchStrategy getStrategyForImportControl(Attributes attributes) { 337 final String returnValue = attributes.getValue(STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME); 338 MismatchStrategy strategyOnMismatch = MismatchStrategy.DISALLOWED; 339 if (STRATEGY_ON_MISMATCH_ALLOWED_VALUE.equals(returnValue)) { 340 strategyOnMismatch = MismatchStrategy.ALLOWED; 341 } 342 return strategyOnMismatch; 343 } 344 345 /** 346 * Utility to get a strategyOnMismatch property for "subpackage" tag. 347 * 348 * @param attributes collect to get attribute from. 349 * @return the value of the attribute. 350 */ 351 private static MismatchStrategy getStrategyForSubpackage(Attributes attributes) { 352 final String returnValue = attributes.getValue(STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME); 353 MismatchStrategy strategyOnMismatch = MismatchStrategy.DELEGATE_TO_PARENT; 354 if (STRATEGY_ON_MISMATCH_ALLOWED_VALUE.equals(returnValue)) { 355 strategyOnMismatch = MismatchStrategy.ALLOWED; 356 } 357 else if (STRATEGY_ON_MISMATCH_DISALLOWED_VALUE.equals(returnValue)) { 358 strategyOnMismatch = MismatchStrategy.DISALLOWED; 359 } 360 return strategyOnMismatch; 361 } 362 363 /** 364 * Utility to safely get an attribute. If it does not exist an exception 365 * is thrown. 366 * 367 * @param attributes collect to get attribute from. 368 * @param name name of the attribute to get. 369 * @return the value of the attribute. 370 * @throws SAXException if the attribute does not exist. 371 */ 372 private static String safeGet(Attributes attributes, String name) 373 throws SAXException { 374 final String returnValue = attributes.getValue(name); 375 if (returnValue == null) { 376 // -@cs[IllegalInstantiation] SAXException is in the overridden method signature 377 // of the only method which calls the current one 378 throw new SAXException("missing attribute " + name); 379 } 380 return returnValue; 381 } 382 383}