package org.infernus.idea.checkstyle.checker; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.infernus.idea.checkstyle.CheckStyleBundle; import org.infernus.idea.checkstyle.exception.CheckStylePluginException; import org.infernus.idea.checkstyle.exception.CheckstyleToolException; import org.infernus.idea.checkstyle.model.ConfigurationLocation; import org.infernus.idea.checkstyle.util.Notifications; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.util.HashMap; import java.util.Map; import java.util.Optional; import static org.infernus.idea.checkstyle.util.Strings.isBlank; /** * Creates Checkers. Registered as projectService in {@code plugin.xml}. */ public class CheckerFactory { private static final Log LOG = LogFactory.getLog(CheckerFactory.class); private final Project project; private final CheckerFactoryCache cache; public CheckerFactory(@NotNull final Project project, @NotNull final CheckerFactoryCache cache) { this.project = project; this.cache = cache; } public void verify(final ConfigurationLocation location) throws IOException { checker(null, location); } public Optional<CheckStyleChecker> checker(@Nullable final Module module, @NotNull final ConfigurationLocation location) { LOG.debug("Getting CheckStyle checker with location " + location); if (location == null) { return Optional.empty(); } try { final CachedChecker cachedChecker = getOrCreateCachedChecker(location, module); if (cachedChecker != null) { return Optional.of(cachedChecker.getCheckStyleChecker()); } return Optional.empty(); } catch (Exception e) { throw new CheckStylePluginException("Couldn't create Checker from " + location, e); } } private CachedChecker getOrCreateCachedChecker(@NotNull final ConfigurationLocation location, @Nullable final Module module) throws IOException { final Optional<CachedChecker> cachedChecker = cache.get(location, module); if (cachedChecker.isPresent()) { return cachedChecker.get(); } final CachedChecker checker = createChecker(location, module); if (checker != null) { cache.put(location, module, checker); return checker; } return null; } private Map<String, String> addEclipseCsProperties(final ConfigurationLocation location, final Module module, final Map<String, String> properties) throws IOException { addIfAbsent("basedir", basePathFor(module), properties); addIfAbsent("project_loc", project.getBasePath(), properties); addIfAbsent("workspace_loc", project.getBasePath(), properties); final String locationBaseDir = Optional.ofNullable(location.getBaseDir()) .map(File::toString) .orElseGet(project::getBasePath); addIfAbsent("config_loc", locationBaseDir, properties); addIfAbsent("samedir", locationBaseDir, properties); return properties; } private String basePathFor(final Module module) { if (module != null) { final File moduleFile = new File(module.getModuleFilePath()); if (moduleFile.getParent() != null && moduleFile.getParentFile().exists()) { return moduleFile.getParentFile().getAbsolutePath(); } } return project.getBasePath(); } private void addIfAbsent(final String key, final String value, final Map<String, String> properties) { if (isBlank(properties.get(key))) { properties.put(key, value); } } private ModuleClassPathBuilder moduleClassPathBuilder() { return ServiceManager.getService(project, ModuleClassPathBuilder.class); } private CachedChecker createChecker(@NotNull final ConfigurationLocation location, @Nullable final Module module) { final ListPropertyResolver propertyResolver; try { final Map<String, String> properties = removeEmptyProperties(location.getProperties()); propertyResolver = new ListPropertyResolver( addEclipseCsProperties(location, module, properties)); } catch (IOException e) { LOG.info("CheckStyle properties could not be loaded: " + location.getLocation(), e); return blacklistAndShowMessage(location, module, "checkstyle.file-io-failed", location.getLocation()); } final ClassLoader loaderOfCheckedCode = moduleClassPathBuilder().build(module); if (LOG.isDebugEnabled()) { LOG.debug("Call to create new checker."); logProperties(propertyResolver); logClassLoaders(loaderOfCheckedCode); } final Object workerResult = executeWorker(location, module, propertyResolver, loaderOfCheckedCode); if (workerResult instanceof CheckstyleToolException) { return blacklistAndShowMessage(location, module, (CheckstyleToolException) workerResult); } else if (workerResult instanceof IOException) { LOG.info("CheckStyle configuration could not be loaded: " + location.getLocation(), (IOException) workerResult); return blacklistAndShowMessage(location, module, "checkstyle.file-not-found", location.getLocation()); } else if (workerResult instanceof Throwable) { location.blacklist(); throw new CheckStylePluginException("Could not load configuration", (Throwable) workerResult); } return (CachedChecker) workerResult; } private Map<String, String> removeEmptyProperties(final Map<String, String> properties) { Map<String, String> cleanedProperties = new HashMap<>(); for (Map.Entry<String, String> property : properties.entrySet()) { if (!isBlank(property.getValue())) { cleanedProperties.put(property.getKey(), property.getValue()); } } return cleanedProperties; } private Object executeWorker(@NotNull final ConfigurationLocation location, @Nullable final Module module, final ListPropertyResolver resolver, @NotNull final ClassLoader loaderOfCheckedCode) { final CheckerFactoryWorker worker = new CheckerFactoryWorker(location, resolver.getPropertyNamesToValues(), project, module, loaderOfCheckedCode); worker.start(); while (worker.isAlive()) { try { worker.join(); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } } return worker.getResult(); } private CachedChecker blacklistAndShowMessage(final ConfigurationLocation location, final Module module, final String messageKey, final Object... messageArgs) { if (!location.isBlacklisted()) { location.blacklist(); if (module != null) { Notifications.showError(module.getProject(), CheckStyleBundle.message(messageKey, messageArgs)); } else { throw new CheckStylePluginException(CheckStyleBundle.message(messageKey, messageArgs)); } } return null; } private CachedChecker blacklistAndShowMessage(final ConfigurationLocation location, final Module module, final CheckstyleToolException checkstyleException) { if (checkstyleException.getMessage().contains("Unable to instantiate DoubleCheckedLocking")) { return blacklistAndShowMessage(location, module, "checkstyle.double-checked-locking"); } else if (checkstyleException.getMessage().contains("unable to parse configuration stream") && checkstyleException.getCause() != null) { return blacklistAndShowMessage(location, module, checkstyleException.getCause().getMessage()); } return blacklistAndShowMessage(location, module, "checkstyle.parse-failed", checkstyleException.getMessage()); } private void logClassLoaders(final ClassLoader pClassLoader) { if (pClassLoader != null) { ClassLoader currentLoader = pClassLoader; while (currentLoader != null) { if (currentLoader instanceof URLClassLoader) { LOG.debug("+ URLClassLoader: " + currentLoader.getClass().getName()); final URLClassLoader urlLoader = (URLClassLoader) currentLoader; for (final URL url : urlLoader.getURLs()) { LOG.debug(" + URL: " + url); } } else { LOG.debug("+ ClassLoader: " + currentLoader.getClass().getName()); } currentLoader = currentLoader.getParent(); } } } private void logProperties(final ListPropertyResolver resolver) { if (resolver != null) { final Map<String, String> propertiesToValues = resolver.getPropertyNamesToValues(); for (final Map.Entry<String, String> propertyEntry : propertiesToValues.entrySet()) { final String propertyValue = propertyEntry.getValue(); LOG.debug("- Property: " + propertyEntry.getKey() + "=" + propertyValue); } } } }