001package co.codewizards.cloudstore.core.ignore; 002 003import static co.codewizards.cloudstore.core.objectfactory.ObjectFactoryUtil.*; 004import static co.codewizards.cloudstore.core.util.AssertUtil.*; 005import static co.codewizards.cloudstore.core.util.Util.*; 006 007import java.lang.ref.SoftReference; 008import java.util.ArrayList; 009import java.util.Collections; 010import java.util.HashSet; 011import java.util.Iterator; 012import java.util.LinkedHashSet; 013import java.util.LinkedList; 014import java.util.List; 015import java.util.Map; 016import java.util.Set; 017import java.util.WeakHashMap; 018import java.util.regex.Pattern; 019 020import org.slf4j.Logger; 021import org.slf4j.LoggerFactory; 022 023import co.codewizards.cloudstore.core.config.Config; 024import co.codewizards.cloudstore.core.config.ConfigImpl; 025import co.codewizards.cloudstore.core.oio.File; 026import co.codewizards.cloudstore.core.repo.local.LocalRepoHelper; 027 028public class IgnoreRuleManagerImpl implements IgnoreRuleManager { 029 private static final Logger logger = LoggerFactory.getLogger(IgnoreRuleManagerImpl.class); 030 031 private final File directory; 032 private Config config; 033 private List<IgnoreRule> ignoreRules; 034 private Long configVersion; 035 036 private static final Object classMutex = IgnoreRuleManagerImpl.class; 037 private final Object instanceMutex = this; 038 039 private static final long fileRefsCleanPeriod = 60000L; 040 private static long fileRefsCleanLastTimestamp; 041 042 private static final LinkedHashSet<File> fileHardRefs = new LinkedHashSet<>(); 043 private static final int fileHardRefsMaxSize = 10; 044 045 /** 046 * {@link SoftReference}s to the files used in {@link #file2IgnoreRuleManager}. 047 * <p> 048 * There is no {@code SoftHashMap}, hence we use a WeakHashMap combined with the {@code SoftReference}s here. 049 * @see #file2IgnoreRuleManager 050 */ 051 private static final LinkedList<SoftReference<File>> fileSoftRefs = new LinkedList<>(); 052 /** 053 * @see #fileSoftRefs 054 */ 055 private static final Map<File, IgnoreRuleManagerImpl> file2IgnoreRuleManager = new WeakHashMap<>(); 056 057 protected IgnoreRuleManagerImpl(File directory) { 058 this.directory = assertNotNull(directory, "directory"); 059 config = ConfigImpl.getInstanceForDirectory(this.directory); 060 } 061 062 private static void cleanFileRefs() { 063 synchronized (classMutex) { 064 if (System.currentTimeMillis() - fileRefsCleanLastTimestamp < fileRefsCleanPeriod) 065 return; 066 067 for (final Iterator<SoftReference<File>> it = fileSoftRefs.iterator(); it.hasNext(); ) { 068 final SoftReference<File> fileRef = it.next(); 069 if (fileRef.get() == null) 070 it.remove(); 071 } 072 fileRefsCleanLastTimestamp = System.currentTimeMillis(); 073 } 074 } 075 076 public static IgnoreRuleManager getInstanceForDirectory(final File directory) { 077 assertNotNull(directory, "directory"); 078 cleanFileRefs(); 079 080 File irm_dir = null; 081 IgnoreRuleManagerImpl irm; 082 synchronized (classMutex) { 083 irm = file2IgnoreRuleManager.get(directory); 084 if (irm != null) { 085 irm_dir = irm.directory; 086 if (irm_dir == null) // very unlikely, but it actually *can* happen. 087 irm = null; // we try to make it extremely probable that the Config we return does have a valid file reference. 088 } 089 090 if (irm == null) { 091 final File localRoot = LocalRepoHelper.getLocalRootContainingFile(directory); 092 if (localRoot == null) 093 throw new IllegalArgumentException("directory is not inside a repository: " + directory.getAbsolutePath()); 094 095 irm = new IgnoreRuleManagerImpl(directory); 096 file2IgnoreRuleManager.put(directory, irm); 097 fileSoftRefs.add(new SoftReference<File>(directory)); 098 irm_dir = irm.directory; 099 } 100 assertNotNull(irm_dir, "irm_dir"); 101 } 102 refreshFileHardRefAndCleanOldHardRefs(irm_dir); 103 return irm; 104 } 105 106 107 public List<IgnoreRule> getIgnoreRules() { 108 refreshFileHardRefAndCleanOldHardRefs(); 109 synchronized (instanceMutex) { 110 final Long newConfigVersion = config.getVersion(); 111 if (! equal(configVersion, newConfigVersion)) 112 ignoreRules = null; 113 114 if (ignoreRules == null) { 115 final Set<String> ignoreRuleIds = getIgnoreRuleIds(); 116 final List<IgnoreRule> result = new ArrayList<>(ignoreRuleIds.size()); 117 for (final String ignoreRuleId : ignoreRuleIds) { 118 final IgnoreRule ignoreRule = loadIgnoreRule(ignoreRuleId); 119 if (ignoreRule != null) 120 result.add(ignoreRule); 121 } 122 configVersion = newConfigVersion; 123 ignoreRules = Collections.unmodifiableList(result); 124 logger.debug("getIgnoreRules: Loaded for newConfigVersion={}: {}", newConfigVersion, ignoreRules); 125 } 126 return ignoreRules; 127 } 128 } 129 130 private Set<String> getIgnoreRuleIds() { 131 final Set<String> result = new HashSet<>(); 132 final Map<String, List<String>> key2Groups = config.getKey2GroupsMatching(Pattern.compile("ignore\\[([^]]*)\\].*")); 133 for (final List<String> groups : key2Groups.values()) { 134 final String ignoreRuleId = groups.get(0); 135 result.add(ignoreRuleId); 136 } 137 return result; 138 } 139 140 @Override 141 public boolean isIgnored(final File file) { 142 final String fileName = assertNotNull(file, "file").getName(); 143 144 if (! directory.equals(file.getParentFile())) 145 throw new IllegalArgumentException(String.format("file '%s' is not located within parent-directory '%s'!", 146 file.getAbsolutePath(), directory.getAbsolutePath())); 147 148 if (fileName.equalsIgnoreCase(Config.PROPERTIES_FILE_NAME_FOR_DIRECTORY_LOCAL)) 149 return true; 150 151 if (fileName.equalsIgnoreCase(Config.PROPERTIES_FILE_NAME_FOR_DIRECTORY)) 152 return false; // https://github.com/cloudstore/cloudstore/issues/60 153 154 for (final IgnoreRule ignoreRule : getIgnoreRules()) { 155 if (! ignoreRule.isEnabled()) 156 continue; 157 158 boolean matches = ignoreRule.getNameRegexPattern().matcher(fileName).matches(); 159 if (matches) 160 return true; 161 } 162 return false; 163 } 164 165 private IgnoreRule loadIgnoreRule(final String ignoreRuleId) { 166 assertNotNull(ignoreRuleId, "ignoreRuleId"); 167 String namePattern = config.getProperty(getConfigKeyNamePattern(ignoreRuleId), null); 168 final String nameRegex = config.getProperty(getConfigKeyNameRegex(ignoreRuleId), null); 169 170 if (namePattern == null && nameRegex == null) 171 return null; 172 173 if (namePattern != null && nameRegex != null) { 174 logger.warn("loadIgnoreRule: ignoreRuleId={}: namePattern='{}' and nameRegex='{}' are both specified! Ignoring namePattern!", 175 ignoreRuleId, namePattern, nameRegex); 176 namePattern = null; 177 } 178 179 IgnoreRule ignoreRule = createObject(IgnoreRuleImpl.class); 180 ignoreRule.setIgnoreRuleId(ignoreRuleId); 181 ignoreRule.setNamePattern(namePattern); 182 ignoreRule.setNameRegex(nameRegex); 183 ignoreRule.setEnabled(config.getPropertyAsBoolean(getConfigKeyEnabled(ignoreRuleId), true)); 184 ignoreRule.setCaseSensitive(config.getPropertyAsBoolean(getConfigKeyCaseSensitive(ignoreRuleId), false)); 185 return ignoreRule; 186 } 187 188 private String getConfigKeyNamePattern(String ignoreRuleId) { 189 return getConfigKeyIgnorePrefix(ignoreRuleId) + "namePattern"; 190 } 191 192 private String getConfigKeyNameRegex(String ignoreRuleId) { 193 return getConfigKeyIgnorePrefix(ignoreRuleId) + "nameRegex"; 194 } 195 196 private String getConfigKeyEnabled(String ignoreRuleId) { 197 return getConfigKeyIgnorePrefix(ignoreRuleId) + "enabled"; 198 } 199 200 private String getConfigKeyCaseSensitive(String ignoreRuleId) { 201 return getConfigKeyIgnorePrefix(ignoreRuleId) + "caseSensitive"; 202 } 203 204 private String getConfigKeyIgnorePrefix(String ignoreRuleId) { 205 assertNotNull(ignoreRuleId, "ignoreRuleId"); 206 return "ignore[" + ignoreRuleId + "]."; 207 } 208 209 private static final void refreshFileHardRefAndCleanOldHardRefs(final IgnoreRuleManagerImpl ignoreRuleManager) { 210 final File dir = assertNotNull(ignoreRuleManager, "ignoreRuleManager").directory; 211 if (dir != null) 212 refreshFileHardRefAndCleanOldHardRefs(dir); 213 } 214 215 private final void refreshFileHardRefAndCleanOldHardRefs() { 216 refreshFileHardRefAndCleanOldHardRefs(this); 217 } 218 219 private static final void refreshFileHardRefAndCleanOldHardRefs(final File dir) { 220 assertNotNull(dir, "dir"); 221 synchronized (fileHardRefs) { 222 // make sure the current dir is at the end of fileHardRefs 223 fileHardRefs.remove(dir); 224 fileHardRefs.add(dir); 225 226 // remove the first entry until size does not exceed limit anymore. 227 while (fileHardRefs.size() > fileHardRefsMaxSize) 228 fileHardRefs.remove(fileHardRefs.iterator().next()); 229 } 230 } 231}