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}