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}