package org.infernus.idea.checkstyle.model;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ContentEntry;
import com.intellij.openapi.roots.ModuleRootManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.infernus.idea.checkstyle.util.CheckStyleEntityResolver;
import org.infernus.idea.checkstyle.util.Objects;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import static java.lang.Math.max;
import static java.lang.System.currentTimeMillis;
import static org.infernus.idea.checkstyle.util.Strings.isBlank;
/**
* Bean encapsulating a configuration source.
*/
public abstract class ConfigurationLocation implements Cloneable, Comparable<ConfigurationLocation> {
private static final Log LOG = LogFactory.getLog(ConfigurationLocation.class);
private static final long BLACKLIST_TIME_MS = 1000 * 60;
private final Map<String, String> properties = new HashMap<>();
private final ConfigurationType type;
private String location;
private String description;
private boolean propertiesCheckedThisSession;
private long blacklistedUntil;
public ConfigurationLocation(final ConfigurationType type) {
if (type == null) {
throw new IllegalArgumentException("A type is required");
}
this.type = type;
}
/**
* Get the base directory for this checkstyle file. If null then the project directory is assumed.
*
* @return the base directory for the file, or null if not applicable to the location type.
*/
public File getBaseDir() {
return null;
}
public ConfigurationType getType() {
return type;
}
public String getLocation() {
return location;
}
public void setLocation(final String location) {
if (isBlank(location)) {
throw new IllegalArgumentException("A non-blank location is required");
}
this.location = location;
if (description == null) {
description = location;
}
this.propertiesCheckedThisSession = false;
}
public String getDescription() {
return description;
}
public void setDescription(@Nullable final String description) {
if (description == null) {
this.description = location;
} else {
this.description = description;
}
}
public Map<String, String> getProperties() throws IOException {
if (!propertiesCheckedThisSession) {
resolveFile();
}
return Collections.unmodifiableMap(properties);
}
public void setProperties(final Map<String, String> newProperties) {
properties.clear();
if (newProperties == null) {
return;
}
properties.putAll(newProperties);
this.propertiesCheckedThisSession = false;
}
public void reset() {
propertiesCheckedThisSession = false;
blacklistedUntil = 0L;
}
/**
* Extract all settable properties from the given configuration file.
*
* @param inputStream the configuration file.
* @return the property names.
*/
private List<String> extractProperties(final InputStream inputStream) {
if (inputStream != null) {
try {
final SAXBuilder saxBuilder = new SAXBuilder();
saxBuilder.setEntityResolver(new CheckStyleEntityResolver());
final Document configDoc = saxBuilder.build(inputStream);
return extractProperties(configDoc.getRootElement());
} catch (Exception e) {
LOG.warn("CheckStyle file could not be parsed for properties.", e);
}
}
return new ArrayList<>();
}
/**
* Extract all settable properties from the given configuration element.
*
* @param element the configuration element.
* @return the settable property names.
*/
@SuppressWarnings("unchecked")
private List<String> extractProperties(final Element element) {
final List<String> propertyNames = new ArrayList<>();
if (element != null) {
extractPropertyNames(element, propertyNames);
for (final Element child : element.getChildren()) {
propertyNames.addAll(extractProperties(child));
}
}
return propertyNames;
}
private void extractPropertyNames(final Element element, final List<String> propertyNames) {
if (!"property".equals(element.getName())) {
return;
}
final String value = element.getAttributeValue("value");
if (value == null) {
return;
}
final int propertyStart = value.indexOf("${");
final int propertyEnd = value.indexOf('}');
if (propertyStart >= 0 && propertyEnd >= 0) {
final String propertyName = value.substring(
propertyStart + 2, propertyEnd);
propertyNames.add(propertyName);
}
}
/**
* Resolve this location to a file.
*
* @return the file to load.
* @throws IOException if the file cannot be loaded.
*/
public InputStream resolve() throws IOException {
InputStream is = resolveFile();
if (!propertiesCheckedThisSession) {
final List<String> propertiesInFile = extractProperties(is);
for (final String propertyName : propertiesInFile) {
if (!properties.containsKey(propertyName)) {
properties.put(propertyName, "");
}
}
properties.keySet().removeIf(propertyName -> !propertiesInFile.contains(propertyName));
try {
is.reset();
} catch (IOException e) {
is = resolveFile(); // JAR IS doesn't support this, for instance
}
propertiesCheckedThisSession = true;
}
return is;
}
@Nullable
public String resolveAssociatedFile(final String filename,
final Project project,
final Module module) throws IOException {
if (filename == null) {
return null;
} else if (new File(filename).exists()) {
return filename;
}
return findFile(filename, project, module);
}
private String findFile(final String fileName,
final Project project,
final Module module) {
if (fileName == null
|| "".equals(fileName.trim())
|| fileName.toLowerCase().startsWith("http://")
|| fileName.toLowerCase().startsWith("https://")) {
return fileName;
}
File targetFile = checkCommonPathsForTarget(fileName, project, module);
if (targetFile != null) {
return targetFile.getAbsolutePath();
}
return null;
}
private File checkCommonPathsForTarget(final String fileName,
final Project project,
final Module module) {
File targetFile = checkRelativeToRulesFile(fileName);
if (module != null) {
if (targetFile == null) {
targetFile = checkModuleContentRoots(module, fileName);
}
if (targetFile == null) {
targetFile = checkModuleFile(module, fileName);
}
}
if (targetFile == null) {
targetFile = checkProjectBaseDir(project, fileName);
}
return targetFile;
}
private File checkRelativeToRulesFile(final String fileName) {
if (getBaseDir() != null) {
final File configFileRelativePath = new File(getBaseDir(), fileName);
if (configFileRelativePath.exists()) {
return configFileRelativePath;
}
}
return null;
}
private File checkProjectBaseDir(final Project project,
final String fileName) {
if (project.getBaseDir() != null) {
final File projectRelativePath = new File(project.getBaseDir().getPath(), fileName);
if (projectRelativePath.exists()) {
return projectRelativePath;
}
}
return null;
}
private File checkModuleFile(final Module module,
final String fileName) {
if (module.getModuleFile() != null) {
final File moduleRelativePath = new File(module.getModuleFile().getParent().getPath(), fileName);
if (moduleRelativePath.exists()) {
return moduleRelativePath;
}
}
return null;
}
private File checkModuleContentRoots(final Module module, final String fileName) {
ModuleRootManager rootManager = ModuleRootManager.getInstance(module);
if (rootManager.getContentEntries().length > 0) {
for (final ContentEntry contentEntry : rootManager.getContentEntries()) {
if (contentEntry.getFile() == null) {
continue;
}
final File contentEntryPath = new File(contentEntry.getFile().getPath(), fileName);
if (contentEntryPath.exists()) {
return contentEntryPath;
}
}
}
return null;
}
public final boolean hasChangedFrom(final ConfigurationLocation configurationLocation) throws IOException {
return configurationLocation == null
|| !equals(configurationLocation)
|| !getProperties().equals(configurationLocation.getProperties());
}
public String getDescriptor() {
assert location != null;
assert description != null;
return type + ":" + location + ":" + description;
}
/**
* Resolve this location to a file.
*
* @return the file to load.
* @throws IOException if the file cannot be loaded.
*/
@NotNull
protected abstract InputStream resolveFile() throws IOException;
@Override
public abstract Object clone();
ConfigurationLocation cloneCommonPropertiesTo(final ConfigurationLocation cloned) {
cloned.setDescription(getDescription());
cloned.setLocation(getLocation());
try {
cloned.setProperties(new HashMap<>(getProperties()));
} catch (IOException e) {
throw new RuntimeException("Failed to resolve properties for " + this);
}
return cloned;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final ConfigurationLocation that = (ConfigurationLocation) o;
if (description != null ? !description.equals(that.description) : that.description != null) {
return false;
}
if (location != null ? !location.equals(that.location) : that.location != null) {
return false;
}
if (type != that.type) {
return false;
}
return true;
}
@Override
public final int hashCode() {
int result = type != null ? type.hashCode() : 0;
result = 31 * result + (location != null ? location.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
return result;
}
@Override
public String toString() {
assert description != null;
return description;
}
@Override
public int compareTo(@NotNull final ConfigurationLocation configurationLocation) {
return Objects.compare(getDescription(), configurationLocation.getDescription());
}
public boolean isBlacklisted() {
return blacklistedUntil > currentTimeMillis();
}
public long blacklistedForSeconds() {
return max((blacklistedUntil - currentTimeMillis()) / 1000, 0);
}
public void blacklist() {
blacklistedUntil = currentTimeMillis() + BLACKLIST_TIME_MS;
}
public void removeFromBlacklist() {
blacklistedUntil = 0L;
}
}