package org.infernus.idea.checkstyle.service.cmd; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.puppycrawl.tools.checkstyle.ConfigurationLoader; import com.puppycrawl.tools.checkstyle.DefaultConfiguration; import com.puppycrawl.tools.checkstyle.PropertyResolver; import com.puppycrawl.tools.checkstyle.api.CheckstyleException; import com.puppycrawl.tools.checkstyle.api.Configuration; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.infernus.idea.checkstyle.exception.CheckstyleServiceException; import org.infernus.idea.checkstyle.model.ConfigurationLocation; import org.infernus.idea.checkstyle.service.IgnoringResolver; import org.infernus.idea.checkstyle.service.SimpleResolver; import org.infernus.idea.checkstyle.service.entities.CsConfigObject; import org.infernus.idea.checkstyle.service.entities.HasCsConfig; import org.jetbrains.annotations.NotNull; import org.xml.sax.InputSource; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collections; import java.util.HashMap; import java.util.Map; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; import static org.infernus.idea.checkstyle.CheckStyleBundle.message; import static org.infernus.idea.checkstyle.util.Notifications.showError; import static org.infernus.idea.checkstyle.util.Notifications.showWarning; import static org.infernus.idea.checkstyle.util.Strings.isBlank; /** * Load a Checkstyle configuration file. */ public class OpLoadConfiguration implements CheckstyleCommand<HasCsConfig> { private static final Log LOG = LogFactory.getLog(OpLoadConfiguration.class); private static final String TREE_WALKER_ELEMENT = "TreeWalker"; private static final Map<String, String> FILENAME_REPLACEMENTS = buildReplacementsMap(); private final RulesContainer rulesContainer; private final PropertyResolver resolver; private final Project project; private final Module module; public OpLoadConfiguration(@NotNull final ConfigurationLocation configurationLocation, @NotNull final Project project) { this(configurationLocation, null, project, null); } public OpLoadConfiguration(@NotNull final ConfigurationLocation configurationLocation, final Map<String, String> properties, @NotNull final Project project) { this(configurationLocation, properties, project, null); } public OpLoadConfiguration(final ConfigurationLocation configurationLocation, final Map<String, String> properties, @NotNull final Project project, final Module module) { this(new ConfigurationLocationRulesContainer(configurationLocation, project), properties, project, module); } public OpLoadConfiguration(@NotNull final VirtualFile rulesFile, @NotNull final Project project) { this(rulesFile, null, project); } public OpLoadConfiguration(@NotNull final VirtualFile rulesFile, final Map<String, String> properties, @NotNull final Project project) { this(new VirtualFileRulesContainer(rulesFile), properties, project, null); } public OpLoadConfiguration(@NotNull final String fileContent, @NotNull final Project project) { this(new ContentRulesContainer(fileContent), null, project, null); } private OpLoadConfiguration(final RulesContainer rulesContainer, final Map<String, String> properties, final Project project, final Module module) { this.rulesContainer = rulesContainer; this.module = module; this.project = project; if (properties != null) { resolver = new SimpleResolver(properties); } else { resolver = new IgnoringResolver(); } } private static Map<String, String> buildReplacementsMap() { Map<String, String> result = new HashMap<>(); result.put("RegexpHeader", "headerFile"); result.put("Header", "headerFile"); result.put("SuppressionFilter", "file"); result.put("ImportControl", "file"); return Collections.unmodifiableMap(result); } @Override public HasCsConfig execute(@NotNull final Project currentProject) throws CheckstyleException { HasCsConfig result; InputStream is = null; try { is = rulesContainer.inputStream(); Configuration configuration = callLoadConfiguration(is); if (configuration == null) { // from the CS code this state appears to occur when there's no <module> element found // in the input stream throw new CheckstyleException("Couldn't find root module in " + rulesContainer.filePath()); } resolveFilePaths(configuration); result = new CsConfigObject(configuration); } catch (IOException e) { throw new CheckstyleException("Error loading file", e); } finally { IOUtils.closeQuietly(is); } return result; } Configuration callLoadConfiguration(final InputStream inputStream) { boolean inputSourceRequired = false; Method method; try { // This will fail in Checkstyle 6.10, 6.10.1, 6.11, and 6.11.1. The method was re-enabled in 6.11.2. method = ConfigurationLoader.class.getMethod("loadConfiguration", InputSource.class, PropertyResolver.class, boolean.class); inputSourceRequired = true; } catch (NoSuchMethodException e) { try { // Solution for Checkstyle 6.10, 6.10.1, 6.11, and 6.11.1. method = ConfigurationLoader.class.getMethod("loadConfiguration", InputStream.class, PropertyResolver.class, boolean.class); } catch (NoSuchMethodException pE) { throw new CheckstyleServiceException("internal error - Could not call " + ConfigurationLoader.class.getName() + ".loadConfiguration() " + "because the method was not found. New Checkstyle runtime?"); } } Configuration result; try { if (inputSourceRequired) { result = (Configuration) method.invoke(null, new InputSource(inputStream), resolver, false); } else { result = (Configuration) method.invoke(null, inputStream, resolver, false); } } catch (IllegalAccessException | InvocationTargetException e) { throw new CheckstyleServiceException("internal error - Failed to call " // + ConfigurationLoader.class.getName() + ".loadConfiguration()", e); } return result; } void resolveFilePaths(@NotNull final Configuration rootElement) throws CheckstyleException { if (!(rootElement instanceof DefaultConfiguration)) { LOG.warn("Root element is of unknown class: " + rootElement.getClass().getName()); return; } for (final Configuration currentChild : rootElement.getChildren()) { if (FILENAME_REPLACEMENTS.containsKey(currentChild.getName())) { checkFilenameForProperty((DefaultConfiguration) rootElement, currentChild, FILENAME_REPLACEMENTS.get(currentChild.getName())); } else if (TREE_WALKER_ELEMENT.equals(currentChild.getName())) { resolveFilePaths(currentChild); } } } private void checkFilenameForProperty(final DefaultConfiguration configRoot, final Configuration configModule, final String propertyName) throws CheckstyleException { final String fileName = getAttributeOrNull(configModule, propertyName); if (!isBlank(fileName)) { try { resolveAndUpdateFile(configRoot, configModule, propertyName, fileName); } catch (IOException e) { showError(project, message("checkstyle.checker-failed", e.getMessage())); } } } private void resolveAndUpdateFile(final DefaultConfiguration configRoot, final Configuration configModule, final String propertyName, final String fileName) throws IOException { final String resolvedFile = rulesContainer.resolveAssociatedFile(fileName, module); if (resolvedFile == null || !resolvedFile.equals(fileName)) { configRoot.removeChild(configModule); if (resolvedFile != null) { configRoot.addChild(elementWithUpdatedFile(resolvedFile, configModule, propertyName)); } else if (isNotOptional(configModule)) { showWarning(project, message(format("checkstyle.not-found.%s", configModule.getName()))); } } } private boolean isNotOptional(final Configuration configModule) { return !"true".equalsIgnoreCase(getAttributeOrNull(configModule, "optional")); } private String getAttributeOrNull(final Configuration element, final String attributeName) { try { return element.getAttribute(attributeName); } catch (CheckstyleException e) { return null; } } private DefaultConfiguration elementWithUpdatedFile(@NotNull final String filename, @NotNull final Configuration originalElement, @NotNull final String propertyName) { // The CheckStyle API won't allow attribute values to be changed, only appended to, // hence we must recreate the node. final DefaultConfiguration target = new DefaultConfiguration(originalElement.getName()); copyChildren(originalElement, target); copyMessages(originalElement, target); copyAttributes(originalElement, propertyName, target); target.addAttribute(propertyName, filename); return target; } private void copyAttributes(@NotNull final Configuration source, @NotNull final String propertyName, @NotNull final DefaultConfiguration target) { if (source.getAttributeNames() != null) { for (String attributeName : source.getAttributeNames()) { if (attributeName.equals(propertyName)) { continue; } target.addAttribute(attributeName, getAttributeOrNull(source, attributeName)); } } } private void copyMessages(@NotNull final Configuration source, @NotNull final DefaultConfiguration target) { if (source.getMessages() != null) { for (String messageKey : source.getMessages().keySet()) { target.addMessage(messageKey, source.getMessages().get(messageKey)); } } } private void copyChildren(@NotNull final Configuration source, @NotNull final DefaultConfiguration target) { if (source.getChildren() != null) { for (Configuration child : source.getChildren()) { target.addChild(child); } } } private interface RulesContainer { default String filePath() { return null; } InputStream inputStream() throws IOException; default String resolveAssociatedFile(final String fileName, final Module module) throws IOException { return null; } } private static class ConfigurationLocationRulesContainer implements RulesContainer { private final ConfigurationLocation configurationLocation; private final Project project; ConfigurationLocationRulesContainer(final ConfigurationLocation configurationLocation, final Project project) { this.configurationLocation = configurationLocation; this.project = project; } @Override public String filePath() { return configurationLocation.getLocation(); } @Override public InputStream inputStream() throws IOException { return configurationLocation.resolve(); } public String resolveAssociatedFile(final String fileName, final Module module) throws IOException { return configurationLocation.resolveAssociatedFile(fileName, project, module); } } private static class VirtualFileRulesContainer implements RulesContainer { private final VirtualFile virtualFile; VirtualFileRulesContainer(final VirtualFile virtualFile) { this.virtualFile = virtualFile; } @Override public String filePath() { return virtualFile.getPath(); } @Override public InputStream inputStream() throws IOException { return virtualFile.getInputStream(); } } private static class ContentRulesContainer implements RulesContainer { private final String content; ContentRulesContainer(final String content) { this.content = content; } @Override public InputStream inputStream() throws IOException { return new ByteArrayInputStream(content.getBytes(UTF_8)); } } }