package org.infernus.idea.checkstyle;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.project.Project;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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;
/**
* Makes the Checkstyle tool available to the plugin in the correct version. Registered in {@code plugin.xml}.
* This must be a project-level service because the Checkstyle version is chosen per project.
*/
public class CheckstyleProjectService {
private static final Log LOG = LogFactory.getLog(CheckstyleProjectService.class);
private static final String PROP_FILE = "checkstyle-idea.properties";
private static final String PROP_NAME_JAVA7 = "checkstyle.versions.java7";
private static final String PROP_NAME_JAVA8 = "checkstyle.versions.java8";
/** mock instance which may be set and used by unit tests */
private static CheckstyleProjectService sMock = null;
private final Project project;
private Callable<CheckstyleClassLoader> checkstyleClassLoaderFactory = null;
private CheckstyleClassLoader checkstyleClassLoader = null;
private final SortedSet<String> supportedVersions;
public CheckstyleProjectService(@NotNull final Project project) {
this.project = project;
supportedVersions = readSupportedVersions();
final CheckStyleConfiguration pluginConfig = CheckStyleConfiguration.getInstance(project);
activateCheckstyleVersion(pluginConfig.getCheckstyleVersion(getDefaultVersion()),
pluginConfig.getThirdPartyClassPath());
}
/**
* Read the supported Checkstyle versions from the config properties file.
*
* @return the supported versions which match the Java level of the current JVM
*/
private SortedSet<String> readSupportedVersions() {
final Properties props = readProperties();
final String javaVersion = Runtime.class.getPackage().getSpecificationVersion();
final SortedSet<String> theVersions = new TreeSet<>(new VersionComparator());
theVersions.addAll(readVersions(props, PROP_NAME_JAVA7));
if (!javaVersion.startsWith("1.7")) {
theVersions.addAll(readVersions(props, PROP_NAME_JAVA8));
}
return Collections.unmodifiableSortedSet(theVersions);
}
private Properties readProperties() {
final Properties props = new Properties();
InputStream is = null;
try {
is = getClass().getClassLoader().getResourceAsStream(PROP_FILE);
if (is == null) {
// in unit tests, it seems we need this:
is = Thread.currentThread().getContextClassLoader().getResourceAsStream(PROP_FILE);
}
if (is != null) {
props.load(is);
}
} catch (IllegalArgumentException | IOException e) {
throw new CheckStylePluginException("Internal error: Could not read internal configuration file '"
+ PROP_FILE + "'", e);
} finally {
IOUtils.closeQuietly(is);
}
if (props.isEmpty()) {
throw new CheckStylePluginException("Internal error: Could not read internal configuration file '"
+ PROP_FILE + "'");
}
return props;
}
private Set<String> readVersions(final Properties props, final String propertyName) {
final String propertyValue = props.getProperty(propertyName);
if (Strings.isBlank(propertyValue)) {
throw new CheckStylePluginException("Internal error: Property '" + propertyName + "' missing from "
+ "configuration file '" + PROP_FILE + "'");
}
final String[] versions = propertyValue.trim().split("\\s*,\\s*");
final Set<String> result = new HashSet<>();
for (final String version : versions) {
if (!version.isEmpty()) {
result.add(version);
}
}
if (result.isEmpty()) {
throw new CheckStylePluginException("Internal error: Property '" + propertyName + "' was empty in "
+ "configuration file '" + PROP_FILE + "'");
}
return result;
}
@NotNull
public SortedSet<String> getSupportedVersions() {
return supportedVersions;
}
public boolean isSupportedVersion(@Nullable final String pVersion) {
return pVersion != null && supportedVersions.contains(pVersion);
}
@NotNull
public String getDefaultVersion() {
return supportedVersions.last();
}
public void activateCheckstyleVersion(@Nullable final String pVersion, @Nullable final List<String>
pThirdPartyJars) {
final String version = isSupportedVersion(pVersion) ? pVersion : getDefaultVersion();
synchronized (project) {
checkstyleClassLoaderFactory = new Callable<CheckstyleClassLoader>() {
@Override
public CheckstyleClassLoader call() {
final List<URL> thirdPartyClassPath = toListOfUrls(pThirdPartyJars);
return new CheckstyleClassLoader(project, version, thirdPartyClassPath);
}
@NotNull
private List<URL> toListOfUrls(@Nullable final List<String> pThirdPartyJars) {
List<URL> result = new ArrayList<>();
if (pThirdPartyJars != null) {
for (final String absolutePath : pThirdPartyJars) {
try {
result.add(new File(absolutePath).toURI().toURL());
} catch (MalformedURLException e) {
LOG.warn("Skipping malformed third party class path entry: " + absolutePath, e);
}
}
}
return result;
}
};
checkstyleClassLoader = null;
}
}
public CheckstyleActions getCheckstyleInstance() {
try {
synchronized (project) {
if (checkstyleClassLoader == null) {
checkstyleClassLoader = checkstyleClassLoaderFactory.call();
}
// Don't worry about caching, class loaders do lots of caching.
return checkstyleClassLoader.loadCheckstyleImpl();
}
} catch (CheckStylePluginException e) {
throw e;
} catch (Exception e) {
throw new CheckStylePluginException("internal error", e);
}
}
public static CheckstyleProjectService getInstance(@NotNull final Project pProject) {
CheckstyleProjectService result = sMock;
if (result == null) {
result = ServiceManager.getService(pProject, CheckstyleProjectService.class);
}
return result;
}
public static void activateMock4UnitTesting(@Nullable final CheckstyleProjectService pMock) {
sMock = pMock;
}
}