001package eu.cqse.check.framework.core.registry; 002 003import java.io.File; 004import java.io.IOException; 005import java.net.MalformedURLException; 006import java.net.URL; 007import java.net.URLClassLoader; 008import java.nio.file.FileVisitResult; 009import java.nio.file.Files; 010import java.nio.file.Path; 011import java.nio.file.SimpleFileVisitor; 012import java.nio.file.attribute.BasicFileAttributes; 013import java.util.ArrayList; 014import java.util.Collection; 015import java.util.EnumSet; 016import java.util.HashMap; 017import java.util.List; 018import java.util.Map; 019 020import org.apache.logging.log4j.LogManager; 021import org.apache.logging.log4j.Logger; 022import org.conqat.lib.commons.collections.CollectionUtils; 023import org.conqat.lib.commons.filesystem.ClassPathUtils; 024import org.conqat.lib.commons.filesystem.FileSystemUtils; 025 026import com.teamscale.commons.annotation.ClassIndexUtils; 027 028import eu.cqse.check.framework.core.Check; 029import eu.cqse.check.framework.core.CheckException; 030import eu.cqse.check.framework.core.CheckImplementationBase; 031import eu.cqse.check.framework.core.CheckInfo; 032import eu.cqse.check.framework.core.CheckInstance; 033import eu.cqse.check.framework.core.ECheckParameter; 034import eu.cqse.check.framework.core.phase.IGlobalExtractionPhase; 035import eu.cqse.check.framework.scanner.ELanguage; 036import eu.cqse.check.framework.shallowparser.ShallowParserFactory; 037import eu.cqse.check.util.clang.ClangUtils; 038 039/** 040 * Registry singleton that manages custom checks. 041 */ 042public class CheckRegistry { 043 044 private static final Logger LOGGER = LogManager.getLogger(); 045 046 /** The singleton instance. */ 047 private static CheckRegistry instance = null; 048 049 /** Mapping from check identifiers to the check information. */ 050 private final Map<String, CheckInfo> checksInfo = new HashMap<>(); 051 052 private final Map<String, Class<? extends IGlobalExtractionPhase<?, ?>>> checkPhases = new HashMap<>(); 053 054 /** 055 * Hidden constructor to avoid multiple instantiation, should be accessed via 056 * {@link #getInstance()} in application code. Test for this package may access 057 * the constructor. 058 */ 059 /* package */ CheckRegistry() { 060 // do nothing 061 } 062 063 /** Returns the singleton instance. */ 064 public static synchronized CheckRegistry getInstance() { 065 if (instance == null) { 066 instance = new CheckRegistry(); 067 } 068 return instance; 069 } 070 071 /** 072 * Registers custom checks from the given package. Classes are discovered via 073 * the service loader mechanism. Therefore the checks must be annotated with 074 * {@link Check}. 075 * <p> 076 * Uses the current thread's class loader for loading the checks. This means the 077 * classes directory needs to be already on the class path. 078 */ 079 public void registerChecksByServiceLoader(String fullPackageName) { 080 Iterable<Class<?>> classNames = ClassIndexUtils.getAnnotated(Check.class); 081 for (Class<?> className : classNames) { 082 try { 083 if (className.getPackage().getName().startsWith(fullPackageName)) { 084 loadCheckFromClass(className); 085 } 086 } catch (NoClassDefFoundError | IOException e) { 087 LOGGER.error("Failed to load check from class: " + className, e); 088 } 089 } 090 } 091 092 /** 093 * Registers the given bundle and loads all classes from the given bundle that 094 * implement {@link CheckImplementationBase} and are annotated with 095 * {@link Check}. 096 * <p> 097 * Uses the current thread's class loader for loading the checks. This means the 098 * classes directory needs already on the class path. 099 */ 100 public void registerChecksFromClasspathDirectory(File classesDirectory) { 101 ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 102 try { 103 registerChecksFromClasspathDirectory(classesDirectory, classLoader); 104 } catch (IOException e) { 105 LOGGER.error("Failed to load checks directory: " + classesDirectory, e); 106 } 107 } 108 109 /** 110 * Uses the provided {@link ClassLoader} for loading the checks. 111 * 112 * @see #registerChecksFromClasspathDirectory(File) 113 */ 114 private void registerChecksFromClasspathDirectory(File classesDirectory, ClassLoader classLoader) 115 throws IOException { 116 List<String> classNames = ClassPathUtils.getClassNames(classesDirectory); 117 for (String className : classNames) { 118 try { 119 loadCheckFromClass(classLoader.loadClass(className)); 120 } catch (ClassNotFoundException | NoClassDefFoundError e) { 121 LOGGER.error("Failed to load check from class: " + className, e); 122 } 123 } 124 } 125 126 /** 127 * Searches the given directory for Jar files or classes directories containing 128 * custom checks. 129 * <p> 130 * This supports registering Jars as well as compiled classes in a 'classes' 131 * directory (e.g. from an Eclipse project). Both are searched recursively, but 132 * whenever a folder contains a classes directory scanning for Jars and further 133 * classes directories is skipped. The restriction results from jar files being 134 * placed e.g by Gradle in the 'build' output of source projects, which would 135 * then be picked up and lead to problems. The same applies to the classes 136 * folder in the Gradle build output. 137 * <p> 138 * The found jars and classes directories will be put on the classpath (using a 139 * separate classloader). 140 */ 141 public void registerCheckDirectory(File customChecksDirectory) { 142 if (!customChecksDirectory.canRead()) { 143 LOGGER.error("Cannot read from custom check directory: " + customChecksDirectory.getAbsolutePath()); 144 return; 145 } 146 147 try { 148 Files.walkFileTree(customChecksDirectory.toPath(), new CustomCheckLoader()); 149 } catch (IOException e) { 150 LOGGER.error("Cannot traverse custom check directory: " + customChecksDirectory.getAbsolutePath()); 151 } 152 } 153 154 /** Loads custom checks from the given Jar file. */ 155 public void loadChecksFromJar(File jarFile) { 156 157 try { 158 // Don't use try-with-resources here, because we deliberately want the 159 // URLClassLoader to remain unclosed. 160 // https://docs.oracle.com/javase/7/docs/api/java/net/URLClassLoader.html#close() 161 URLClassLoader classLoader = createClassLoader(jarFile); 162 163 for (String className : FileSystemUtils.listTopLevelClassesInJarFile(jarFile)) { 164 loadCheckFromClass(classLoader.loadClass(className)); 165 } 166 } catch (IOException | ClassNotFoundException e) { 167 LOGGER.error("Error loading custom checks from jar '" + jarFile + "': " + e.toString(), e); 168 } 169 } 170 171 /** Loads custom checks from the given classes directory. */ 172 private void loadChecksFromClasses(File classesDir) { 173 try { 174 registerChecksFromClasspathDirectory(classesDir, createClassLoader(classesDir)); 175 } catch (IOException e) { 176 LOGGER.error("Error loading custom checks classes dir '" + classesDir + "': " + e.toString(), e); 177 } 178 } 179 180 /** Creates a class loader for the jar file or directory path. */ 181 private static URLClassLoader createClassLoader(File classpath) throws MalformedURLException { 182 ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); 183 return new URLClassLoader(new URL[] { classpath.toURI().toURL() }, contextClassLoader); 184 } 185 186 /** 187 * Creates and registers a custom check instance from the given class. If the 188 * given class does not contain a custom check (class not annotated with 189 * {@link Check}), this method does nothing. 190 */ 191 private void loadCheckFromClass(Class<?> clazz) throws IOException { 192 CheckInfo checkInfo; 193 try { 194 checkInfo = CheckLoader.loadFromClass(clazz); 195 } catch (CheckException e) { 196 LOGGER.error(e.getMessage(), e); 197 return; 198 } 199 200 if (checkInfo == null) { 201 return; 202 } 203 204 if (checksInfo.containsKey(checkInfo.getIdentifier())) { 205 LOGGER.error("Check " + checkInfo.getGroupName() + ":" + checkInfo.getName() + " is already registered."); 206 return; 207 } 208 for (Class<? extends IGlobalExtractionPhase<?, ?>> phase : checkInfo.getRequiredPhases()) { 209 Class<? extends IGlobalExtractionPhase<?, ?>> existingPhase = checkPhases.get(phase.getName()); 210 if (existingPhase != null && existingPhase != phase) { 211 throw new IOException( 212 "Check phase '" + phase.getName() + "' registered twice via different classloaders."); 213 } 214 if (checkPhaseIsValid(phase)) { 215 checkPhases.put(phase.getName(), phase); 216 } else { 217 LOGGER.error("Skipping registration of check phase " + phase.getName()); 218 } 219 } 220 checksInfo.put(checkInfo.getIdentifier(), checkInfo); 221 } 222 223 /** 224 * Instantiates and validates the given {@link IGlobalExtractionPhase}. Returns 225 * false if the phase can't be instantiated or if the check phase is not 226 * configured correctly. Logs an error in both cases. 227 */ 228 private static boolean checkPhaseIsValid(Class<? extends IGlobalExtractionPhase<?, ?>> phase) { 229 try { 230 IGlobalExtractionPhase<?, ?> phaseInstance = phase.newInstance(); 231 for (ECheckParameter parameter : phaseInstance.getRequiredContextParameters()) { 232 if (!checkPhaseParameterIsValid(phaseInstance, parameter)) { 233 return false; 234 } 235 } 236 return true; 237 } catch (IllegalAccessException | InstantiationException e) { 238 LOGGER.error("Could not instantiate check phase " + phase.getName(), e); 239 return false; 240 } 241 } 242 243 /** 244 * Returns whether the given ECheckParameter is valid in the given 245 * {@link IGlobalExtractionPhase}. Logs an error if not. 246 */ 247 private static boolean checkPhaseParameterIsValid(IGlobalExtractionPhase<?, ?> phaseInstance, 248 ECheckParameter parameter) { 249 switch (parameter) { 250 case ABSTRACT_SYNTAX_TREE: 251 // fall through intended, both need parsing 252 case TYPE_RESOLUTION: 253 if (!allPhaseLanguagesSupportParsing(phaseInstance)) { 254 return false; 255 } 256 break; 257 case CLANG: 258 if (!allPhaseLanguagesSupportClang(phaseInstance)) { 259 return false; 260 } 261 break; 262 default: 263 LOGGER.error(parameter.name() + " must be a new check parameter. Please add validation case."); 264 return false; 265 } 266 return true; 267 } 268 269 /** 270 * Returns whether all languages selected by the given phaseInstance support 271 * Clang processing. Logs an error if not. 272 */ 273 private static boolean allPhaseLanguagesSupportClang(IGlobalExtractionPhase<?, ?> phaseInstance) { 274 EnumSet<ELanguage> invalidLanguages = phaseInstance.getLanguages(); 275 invalidLanguages.removeAll(ClangUtils.CLANG_ENABLED_LANGUAGES); 276 if (!invalidLanguages.isEmpty()) { 277 LOGGER.error("Check phase " + phaseInstance.getClass().getName() 278 + " requires CLANG but selects languages without clang support: " + invalidLanguages); 279 return false; 280 } 281 return true; 282 } 283 284 /** 285 * Returns whether all languages selected by the given phaseInstance have a 286 * ShallowParser. Logs an error if not. 287 */ 288 private static boolean allPhaseLanguagesSupportParsing(IGlobalExtractionPhase<?, ?> phaseInstance) { 289 EnumSet<ELanguage> invalidLanguages = phaseInstance.getLanguages(); 290 invalidLanguages.removeAll(ShallowParserFactory.getSupportedLanguages()); 291 if (!invalidLanguages.isEmpty()) { 292 LOGGER.error("Check phase " + phaseInstance.getClass().getName() 293 + " requires AST but selects languages without parser: " + invalidLanguages); 294 return false; 295 } 296 return true; 297 } 298 299 /** 300 * Returns the check phase class with the given full qualified name or 301 * <code>null</code> if no such phase exists. 302 */ 303 public Class<? extends IGlobalExtractionPhase<?, ?>> getCheckPhase(String phaseName) { 304 return checkPhases.get(phaseName); 305 } 306 307 /** 308 * Creates check instances for the given check identifiers. 309 * 310 * @throws CheckException 311 * if there is no check for one of the given identifiers 312 */ 313 public Collection<CheckInstance> instantiateChecks(List<String> identifiers) throws CheckException { 314 List<CheckInstance> instances = new ArrayList<>(); 315 for (String identifier : identifiers) { 316 if (identifier.equals( 317 "Code Anomalies#:#Readability#:#Switch statement should have a default label as last switch label")) { 318 // Old check that has been removed in TS-19527, but the migration was not 319 // drop-in, so drop-in upgraded 5.2.0-5.2.5 instances might be affected. 320 // This can removed in 5.3.0+ 321 continue; 322 } 323 CheckInfo checkInfo = checksInfo.get(identifier); 324 if (checkInfo == null) { 325 LOGGER.error("No check with identifier '" + identifier + "' exists, skipping..."); 326 continue; 327 } 328 instances.add(new CheckInstance(checkInfo)); 329 } 330 return instances; 331 } 332 333 /** Returns a list of all registered checks. */ 334 public Collection<CheckInfo> getChecksInfos() { 335 return CollectionUtils.asUnmodifiable(checksInfo.values()); 336 } 337 338 /** 339 * File visitor that scans a file tree and loads custom checks from classes 340 * directories and jar files. 341 */ 342 private final class CustomCheckLoader extends SimpleFileVisitor<Path> { 343 @Override 344 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { 345 Path classes = dir.resolve("classes/java/main"); 346 if (Files.isDirectory(classes)) { 347 LOGGER.info("Registering custom checks from classes: " + classes); 348 loadChecksFromClasses(classes.toFile()); 349 return FileVisitResult.SKIP_SUBTREE; 350 } 351 classes = dir.resolve("classes"); 352 if (Files.isDirectory(classes) 353 && !FileSystemUtils.normalizeSeparators(classes.toString()).contains("/test/")) { 354 LOGGER.info("Registering custom checks from classes: " + classes); 355 loadChecksFromClasses(classes.toFile()); 356 return FileVisitResult.SKIP_SUBTREE; 357 } 358 return FileVisitResult.CONTINUE; 359 } 360 361 @Override 362 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 363 if (file.toString().endsWith(".jar")) { 364 LOGGER.info("Registering custom checks from jar: " + file); 365 loadChecksFromJar(file.toFile()); 366 } 367 return FileVisitResult.CONTINUE; 368 } 369 } 370}