001package co.codewizards.cloudstore.local.persistence;
002
003import static java.util.Objects.*;
004
005import java.util.Collection;
006import java.util.HashMap;
007import java.util.LinkedList;
008import java.util.Map;
009import java.util.UUID;
010
011import javax.jdo.PersistenceManager;
012import javax.jdo.Query;
013
014import org.slf4j.Logger;
015import org.slf4j.LoggerFactory;
016
017import co.codewizards.cloudstore.core.oio.File;
018
019public class RepoFileDao extends Dao<RepoFile, RepoFileDao> {
020        private static final Logger logger = LoggerFactory.getLogger(RepoFileDao.class);
021
022        private Directory localRootDirectory;
023
024        private DirectoryCache directoryCache;
025
026        private static class DirectoryCache {
027                private static final int MAX_SIZE = 50;
028                private final Map<File, Directory> file2DirectoryCache = new HashMap<File, Directory>();
029                private final Map<Directory, File> directory2FileCache = new HashMap<Directory, File>();
030                private final LinkedList<Directory> directoryCacheList = new LinkedList<Directory>();
031
032                public Directory get(final File file) {
033                        return file2DirectoryCache.get(file);
034                }
035
036                public void put(final File file, final Directory directory) {
037                        file2DirectoryCache.put(requireNonNull(file, "file"), requireNonNull(directory, "directory"));
038                        directory2FileCache.put(directory, file);
039                        directoryCacheList.remove(directory);
040                        directoryCacheList.addLast(directory);
041                        removeOldEntriesIfNecessary();
042                }
043
044                public void remove(final Directory directory) {
045                        final File file = directory2FileCache.remove(directory);
046                        file2DirectoryCache.remove(file);
047                }
048
049                public void remove(final File file) {
050                        final Directory directory = file2DirectoryCache.remove(file);
051                        directory2FileCache.remove(directory);
052                }
053
054                private void removeOldEntriesIfNecessary() {
055                        while (directoryCacheList.size() > MAX_SIZE) {
056                                final Directory directory = directoryCacheList.removeFirst();
057                                remove(directory);
058                        }
059                }
060        }
061
062        /**
063         * Get the child of the given {@code parent} with the specified {@code name}.
064         * @param parent the {@link RepoFile#getParent() parent} of the queried child.
065         * @param name the {@link RepoFile#getName() name} of the queried child.
066         * @return the child matching the given criteria; <code>null</code>, if there is no such object in the database.
067         */
068        public RepoFile getChildRepoFile(final RepoFile parent, final String name) {
069                final Query query = pm().newNamedQuery(getEntityClass(), "getChildRepoFile_parent_name");
070                final RepoFile repoFile = (RepoFile) query.execute(parent, name);
071                return repoFile;
072        }
073
074        /**
075         * Get the {@link RepoFile} for the given {@code file} in the file system.
076         * @param localRoot the repository's root directory in the file system. Must not be <code>null</code>.
077         * @param file the file in the file system for which to query the associated {@link RepoFile}. Must not be <code>null</code>.
078         * @return the {@link RepoFile} for the given {@code file} in the file system; <code>null</code>, if no such
079         * object exists in the database.
080         * @throws IllegalArgumentException if one of the parameters is <code>null</code> or if the given {@code file}
081         * is not located inside the repository - i.e. it is not a direct or indirect child of the given {@code localRoot}.
082         */
083        public RepoFile getRepoFile(final File localRoot, final File file) throws IllegalArgumentException {
084                return _getRepoFile(requireNonNull(localRoot, "localRoot"), requireNonNull(file, "file"), file);
085        }
086
087        private RepoFile _getRepoFile(final File localRoot, final File file, final File originallySearchedFile) {
088                if (localRoot.equals(file)) {
089                        return getLocalRootDirectory();
090                }
091
092                final DirectoryCache directoryCache = getDirectoryCache();
093                final Directory directory = directoryCache.get(file);
094                if (directory != null)
095                        return directory;
096
097                final File parentFile = file.getParentFile();
098                if (parentFile == null)
099                        throw new IllegalArgumentException(String.format("Repository '%s' does not contain file '%s'!", localRoot, originallySearchedFile));
100
101                final RepoFile parentRepoFile = _getRepoFile(localRoot, parentFile, originallySearchedFile);
102                final RepoFile result = getChildRepoFile(parentRepoFile, file.getName());
103                if (result instanceof Directory)
104                        directoryCache.put(file, (Directory)result);
105
106                return result;
107        }
108
109        public Directory getLocalRootDirectory() {
110                if (localRootDirectory == null)
111                        localRootDirectory = new LocalRepositoryDao().persistenceManager(pm()).getLocalRepositoryOrFail().getRoot();
112
113                return localRootDirectory;
114        }
115
116        /**
117         * Get the children of the given {@code parent}.
118         * <p>
119         * The children are those {@link RepoFile}s whose {@link RepoFile#getParent() parent} equals the given
120         * {@code parent} parameter.
121         * @param parent the parent whose children are to be queried. This may be <code>null</code>, but since
122         * there is only one single instance with {@code RepoFile.parent} being null - the root directory - this
123         * is usually never <code>null</code>.
124         * @return the children of the given {@code parent}. Never <code>null</code>, but maybe empty.
125         */
126        public Collection<RepoFile> getChildRepoFiles(final RepoFile parent) {
127                final Query query = pm().newNamedQuery(getEntityClass(), "getChildRepoFiles_parent");
128                try {
129                        @SuppressWarnings("unchecked")
130                        final Collection<RepoFile> repoFiles = (Collection<RepoFile>) query.execute(parent);
131                        return load(repoFiles);
132                } finally {
133                        query.closeAll();
134                }
135        }
136
137        /**
138         * Get those {@link RepoFile}s whose {@link RepoFile#getLocalRevision() localRevision} is greater
139         * than the given {@code localRevision}.
140         * @param localRevision the {@link RepoFile#getLocalRevision() localRevision}, after which the files
141         * to be queried where modified.
142         * @param exclLastSyncFromRepositoryId the {@link RepoFile#getLastSyncFromRepositoryId() lastSyncFromRepositoryId}
143         * to exclude from the result set. This is used to prevent changes originating from a repository to be synced back
144         * to its origin (unnecessary and maybe causing a collision there).
145         * See <a href="https://github.com/cloudstore/cloudstore/issues/25">issue 25</a>.
146         * @return those {@link RepoFile}s which were modified after the given {@code localRevision}. Never
147         * <code>null</code>, but maybe empty.
148         */
149        public Collection<RepoFile> getRepoFilesChangedAfterExclLastSyncFromRepositoryId(final long localRevision, final UUID exclLastSyncFromRepositoryId) {
150                requireNonNull(exclLastSyncFromRepositoryId, "exclLastSyncFromRepositoryId");
151                logger.debug("getRepoFilesChangedAfterExclLastSyncFromRepositoryId: localRevision={} exclLastSyncFromRepositoryId={}", localRevision, exclLastSyncFromRepositoryId);
152                final PersistenceManager pm = pm();
153                final FetchPlanBackup fetchPlanBackup = FetchPlanBackup.createFrom(pm);
154                final Query query = pm.newNamedQuery(getEntityClass(), "getRepoFilesChangedAfter_localRevision_exclLastSyncFromRepositoryId");
155                try {
156                        clearFetchGroups();
157                        long startTimestamp = System.currentTimeMillis();
158                        @SuppressWarnings("unchecked")
159                        Collection<RepoFile> repoFiles = (Collection<RepoFile>) query.execute(localRevision, exclLastSyncFromRepositoryId.toString());
160                        logger.debug("getRepoFilesChangedAfterExclLastSyncFromRepositoryId: query.execute(...) took {} ms.", System.currentTimeMillis() - startTimestamp);
161
162                        fetchPlanBackup.restore(pm);
163                        startTimestamp = System.currentTimeMillis();
164                        repoFiles = load(repoFiles);
165                        logger.debug("getRepoFilesChangedAfterExclLastSyncFromRepositoryId: Loading result-set with {} elements took {} ms.", repoFiles.size(), System.currentTimeMillis() - startTimestamp);
166
167                        return repoFiles;
168                } finally {
169                        query.closeAll();
170                        fetchPlanBackup.restore(pm);
171                }
172        }
173
174        @Override
175        public void deletePersistent(final RepoFile entity) {
176                getPersistenceManager().flush();
177                if (entity instanceof Directory)
178                        getDirectoryCache().remove((Directory) entity);
179
180                super.deletePersistent(entity);
181                getPersistenceManager().flush(); // We run *sometimes* into foreign key violations if we don't delete immediately :-(
182        }
183
184        private DirectoryCache getDirectoryCache() {
185                if (directoryCache == null)
186                        directoryCache = new DirectoryCache();
187
188                return directoryCache;
189        }
190}