package org.infernus.idea.checkstyle; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.lang.reflect.Constructor; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Properties; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.project.Project; import com.intellij.util.lang.UrlClassLoader; import org.infernus.idea.checkstyle.csapi.CheckstyleActions; import org.infernus.idea.checkstyle.exception.CheckStylePluginException; import org.infernus.idea.checkstyle.util.Strings; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Loads Checkstyle classes from a given Checkstyle version. */ public class CheckstyleClassLoader { private static final String PROP_FILE = "checkstyle-classpaths.properties"; private static final String CSACTIONS_CLASS = "org.infernus.idea.checkstyle.service.CheckstyleActionsImpl"; /** * <img src="doc-files/CheckstyleClassLoader-1.png"/> */ private static final Pattern CLASSES_URL = Pattern.compile("^(.*?)[/\\\\]classes(?:[/\\\\]main)?[/\\\\]?$"); private final ClassLoader classLoader; private final Project project; public CheckstyleClassLoader(@NotNull final Project pProject, @NotNull final String pCheckstyleVersion, @Nullable final List<URL> pThirdPartyClassPath) { project = pProject; final Properties classPathInfos = loadClassPathInfos(); final String cpProp = classPathInfos.getProperty(pCheckstyleVersion); if (Strings.isBlank(cpProp)) { throw new CheckStylePluginException("Unsupported Checkstyle version: " + pCheckstyleVersion); } List<URL> thirdPartyClassPath = pThirdPartyClassPath != null ? pThirdPartyClassPath : Collections.emptyList(); classLoader = buildClassLoader(cpProp, thirdPartyClassPath); } @NotNull private static Properties loadClassPathInfos() { final Properties result = new Properties(); try (InputStream is = CheckstyleClassLoader.class.getClassLoader().getResourceAsStream(PROP_FILE)) { result.load(is); } catch (IOException e) { throw new CheckStylePluginException("Could not read plugin-internal file: " + PROP_FILE, e); } return result; } @NotNull private ClassLoader buildClassLoader(@NotNull final String pClassPathFromProps, @NotNull final List<URL> pThirdPartyClassPath) { final String basePath = getBasePath(); final File classesDir4UnitTesting = new File(basePath, "classes/csaccess"); final boolean unitTesting = classesDir4UnitTesting.exists(); List<URL> urls = new ArrayList<>(); try { if (unitTesting) { urls.add(classesDir4UnitTesting.toURI().toURL()); } else { urls.add(new File(basePath, "checkstyle/classes").toURI().toURL()); } for (String jar : pClassPathFromProps.trim().split("\\s*;\\s*")) { if (unitTesting) { jar = "tmp/gatherCheckstyleArtifacts" + jar.substring(jar.lastIndexOf('/')); } urls.add(new File(basePath, jar).toURI().toURL()); } } catch (MalformedURLException e) { throw new CheckStylePluginException("internal error", e); } urls.addAll(pThirdPartyClassPath); // The plugin classloader is the new classloader's parent classloader. return new URLClassLoader(urls.toArray(new URL[urls.size()]), getClass().getClassLoader()); } @NotNull private List<URL> getUrls(@NotNull final ClassLoader pClassLoader) { List<URL> result; if (pClassLoader instanceof UrlClassLoader) { // happens normally result = ((UrlClassLoader) pClassLoader).getUrls(); } else if (pClassLoader instanceof URLClassLoader) { // happens in test cases result = Arrays.asList(((URLClassLoader) pClassLoader).getURLs()); } else { throw new CheckStylePluginException("incompatible class loader: " + (pClassLoader != null ? pClassLoader.getClass().getName() : "null")); } return result; } /** * Determine the base path of the plugin. When running in IntelliJ, this is something like * {@code C:/Users/jdoe/.IdeaIC2016.3/config/plugins/CheckStyle-IDEA} (on Windows). When running in a unit test, * it is this project's build directory, for example {@code D:/Documents/Projects/checkstyle-idea/build} (again * on Windows). * * @return the base path, as absolute path */ @NotNull private String getBasePath() { String result = null; try { File pluginDir = new File(PathManager.getPluginsPath(), CheckStylePlugin.ID_PLUGIN); if (pluginDir.exists()) { result = pluginDir.getAbsolutePath(); } } catch (RuntimeException e) { // ok, if this fails, we are in a unit test situation where PathManager is not initialized, which is fine result = null; } // If we could not get the base path from the path manager, deduce it from the current class path: if (result == null) { for (final URL url : getUrls(getClass().getClassLoader())) { String path = url.getPath(); Matcher matcher = CLASSES_URL.matcher(path); if (matcher.find()) { result = matcher.group(1); break; } } if (result != null) { result = urlDecode(result); } } if (result == null) { throw new CheckStylePluginException("Could not determine plugin directory"); } return result; } @NotNull private String urlDecode(final String urlEncodedString) { try { return URLDecoder.decode(urlEncodedString, "UTF-8"); } catch (UnsupportedEncodingException ignored) { return urlEncodedString; } } @NotNull CheckstyleActions loadCheckstyleImpl() { try { Constructor<?> constructor = classLoader.loadClass(CSACTIONS_CLASS).getConstructor(Project.class); return (CheckstyleActions) constructor.newInstance(project); } catch (ReflectiveOperationException e) { throw new CheckStylePluginException("internal error", e); } } }