package co.codewizards.cloudstore.core.ignore;
import static co.codewizards.cloudstore.core.objectfactory.ObjectFactoryUtil.*;
import static co.codewizards.cloudstore.core.util.AssertUtil.*;
import static co.codewizards.cloudstore.core.util.Util.*;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.codewizards.cloudstore.core.config.Config;
import co.codewizards.cloudstore.core.config.ConfigImpl;
import co.codewizards.cloudstore.core.oio.File;
import co.codewizards.cloudstore.core.repo.local.LocalRepoHelper;
public class IgnoreRuleManagerImpl implements IgnoreRuleManager {
private static final Logger logger = LoggerFactory.getLogger(IgnoreRuleManagerImpl.class);
private final File directory;
private Config config;
private List<IgnoreRule> ignoreRules;
private Long configVersion;
private static final Object classMutex = IgnoreRuleManagerImpl.class;
private final Object instanceMutex = this;
private static final long fileRefsCleanPeriod = 60000L;
private static long fileRefsCleanLastTimestamp;
private static final LinkedHashSet<File> fileHardRefs = new LinkedHashSet<>();
private static final int fileHardRefsMaxSize = 10;
/**
* {@link SoftReference}s to the files used in {@link #file2IgnoreRuleManager}.
* <p>
* There is no {@code SoftHashMap}, hence we use a WeakHashMap combined with the {@code SoftReference}s here.
* @see #file2IgnoreRuleManager
*/
private static final LinkedList<SoftReference<File>> fileSoftRefs = new LinkedList<>();
/**
* @see #fileSoftRefs
*/
private static final Map<File, IgnoreRuleManagerImpl> file2IgnoreRuleManager = new WeakHashMap<>();
protected IgnoreRuleManagerImpl(File directory) {
this.directory = assertNotNull(directory, "directory");
config = ConfigImpl.getInstanceForDirectory(this.directory);
}
private static void cleanFileRefs() {
synchronized (classMutex) {
if (System.currentTimeMillis() - fileRefsCleanLastTimestamp < fileRefsCleanPeriod)
return;
for (final Iterator<SoftReference<File>> it = fileSoftRefs.iterator(); it.hasNext(); ) {
final SoftReference<File> fileRef = it.next();
if (fileRef.get() == null)
it.remove();
}
fileRefsCleanLastTimestamp = System.currentTimeMillis();
}
}
public static IgnoreRuleManager getInstanceForDirectory(final File directory) {
assertNotNull(directory, "directory");
cleanFileRefs();
File irm_dir = null;
IgnoreRuleManagerImpl irm;
synchronized (classMutex) {
irm = file2IgnoreRuleManager.get(directory);
if (irm != null) {
irm_dir = irm.directory;
if (irm_dir == null) // very unlikely, but it actually *can* happen.
irm = null; // we try to make it extremely probable that the Config we return does have a valid file reference.
}
if (irm == null) {
final File localRoot = LocalRepoHelper.getLocalRootContainingFile(directory);
if (localRoot == null)
throw new IllegalArgumentException("directory is not inside a repository: " + directory.getAbsolutePath());
irm = new IgnoreRuleManagerImpl(directory);
file2IgnoreRuleManager.put(directory, irm);
fileSoftRefs.add(new SoftReference<File>(directory));
irm_dir = irm.directory;
}
assertNotNull(irm_dir, "irm_dir");
}
refreshFileHardRefAndCleanOldHardRefs(irm_dir);
return irm;
}
public List<IgnoreRule> getIgnoreRules() {
refreshFileHardRefAndCleanOldHardRefs();
synchronized (instanceMutex) {
final Long newConfigVersion = config.getVersion();
if (! equal(configVersion, newConfigVersion))
ignoreRules = null;
if (ignoreRules == null) {
final Set<String> ignoreRuleIds = getIgnoreRuleIds();
final List<IgnoreRule> result = new ArrayList<>(ignoreRuleIds.size());
for (final String ignoreRuleId : ignoreRuleIds) {
final IgnoreRule ignoreRule = loadIgnoreRule(ignoreRuleId);
if (ignoreRule != null)
result.add(ignoreRule);
}
configVersion = newConfigVersion;
ignoreRules = Collections.unmodifiableList(result);
logger.debug("getIgnoreRules: Loaded for newConfigVersion={}: {}", newConfigVersion, ignoreRules);
}
return ignoreRules;
}
}
private Set<String> getIgnoreRuleIds() {
final Set<String> result = new HashSet<>();
final Map<String, List<String>> key2Groups = config.getKey2GroupsMatching(Pattern.compile("ignore\\[([^]]*)\\].*"));
for (final List<String> groups : key2Groups.values()) {
final String ignoreRuleId = groups.get(0);
result.add(ignoreRuleId);
}
return result;
}
@Override
public boolean isIgnored(final File file) {
final String fileName = assertNotNull(file, "file").getName();
if (! directory.equals(file.getParentFile()))
throw new IllegalArgumentException(String.format("file '%s' is not located within parent-directory '%s'!",
file.getAbsolutePath(), directory.getAbsolutePath()));
if (fileName.equalsIgnoreCase(Config.PROPERTIES_FILE_NAME_FOR_DIRECTORY_LOCAL))
return true;
if (fileName.equalsIgnoreCase(Config.PROPERTIES_FILE_NAME_FOR_DIRECTORY))
return false; // https://github.com/cloudstore/cloudstore/issues/60
for (final IgnoreRule ignoreRule : getIgnoreRules()) {
if (! ignoreRule.isEnabled())
continue;
boolean matches = ignoreRule.getNameRegexPattern().matcher(fileName).matches();
if (matches)
return true;
}
return false;
}
private IgnoreRule loadIgnoreRule(final String ignoreRuleId) {
assertNotNull(ignoreRuleId, "ignoreRuleId");
String namePattern = config.getProperty(getConfigKeyNamePattern(ignoreRuleId), null);
final String nameRegex = config.getProperty(getConfigKeyNameRegex(ignoreRuleId), null);
if (namePattern == null && nameRegex == null)
return null;
if (namePattern != null && nameRegex != null) {
logger.warn("loadIgnoreRule: ignoreRuleId={}: namePattern='{}' and nameRegex='{}' are both specified! Ignoring namePattern!",
ignoreRuleId, namePattern, nameRegex);
namePattern = null;
}
IgnoreRule ignoreRule = createObject(IgnoreRuleImpl.class);
ignoreRule.setIgnoreRuleId(ignoreRuleId);
ignoreRule.setNamePattern(namePattern);
ignoreRule.setNameRegex(nameRegex);
ignoreRule.setEnabled(config.getPropertyAsBoolean(getConfigKeyEnabled(ignoreRuleId), true));
ignoreRule.setCaseSensitive(config.getPropertyAsBoolean(getConfigKeyCaseSensitive(ignoreRuleId), false));
return ignoreRule;
}
private String getConfigKeyNamePattern(String ignoreRuleId) {
return getConfigKeyIgnorePrefix(ignoreRuleId) + "namePattern";
}
private String getConfigKeyNameRegex(String ignoreRuleId) {
return getConfigKeyIgnorePrefix(ignoreRuleId) + "nameRegex";
}
private String getConfigKeyEnabled(String ignoreRuleId) {
return getConfigKeyIgnorePrefix(ignoreRuleId) + "enabled";
}
private String getConfigKeyCaseSensitive(String ignoreRuleId) {
return getConfigKeyIgnorePrefix(ignoreRuleId) + "caseSensitive";
}
private String getConfigKeyIgnorePrefix(String ignoreRuleId) {
assertNotNull(ignoreRuleId, "ignoreRuleId");
return "ignore[" + ignoreRuleId + "].";
}
private static final void refreshFileHardRefAndCleanOldHardRefs(final IgnoreRuleManagerImpl ignoreRuleManager) {
final File dir = assertNotNull(ignoreRuleManager, "ignoreRuleManager").directory;
if (dir != null)
refreshFileHardRefAndCleanOldHardRefs(dir);
}
private final void refreshFileHardRefAndCleanOldHardRefs() {
refreshFileHardRefAndCleanOldHardRefs(this);
}
private static final void refreshFileHardRefAndCleanOldHardRefs(final File dir) {
assertNotNull(dir, "dir");
synchronized (fileHardRefs) {
// make sure the current dir is at the end of fileHardRefs
fileHardRefs.remove(dir);
fileHardRefs.add(dir);
// remove the first entry until size does not exceed limit anymore.
while (fileHardRefs.size() > fileHardRefsMaxSize)
fileHardRefs.remove(fileHardRefs.iterator().next());
}
}
}