001package co.codewizards.cloudstore.local.persistence;
002
003import static co.codewizards.cloudstore.core.util.AssertUtil.*;
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.Query;
012
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015
016import co.codewizards.cloudstore.core.oio.File;
017import co.codewizards.cloudstore.core.util.AssertUtil;
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(assertNotNull(file, "file"), assertNotNull(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(AssertUtil.assertNotNull(localRoot, "localRoot"), AssertUtil.assertNotNull(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
131                        Collection<RepoFile> repoFiles = (Collection<RepoFile>) query.execute(parent);
132                        return load(repoFiles);
133                } finally {
134                        query.closeAll();
135                }
136        }
137
138        /**
139         * Get those {@link RepoFile}s whose {@link RepoFile#getLocalRevision() localRevision} is greater
140         * than the given {@code localRevision}.
141         * @param localRevision the {@link RepoFile#getLocalRevision() localRevision}, after which the files
142         * to be queried where modified.
143         * @param exclLastSyncFromRepositoryId the {@link RepoFile#getLastSyncFromRepositoryId() lastSyncFromRepositoryId}
144         * to exclude from the result set. This is used to prevent changes originating from a repository to be synced back
145         * to its origin (unnecessary and maybe causing a collision there).
146         * See <a href="https://github.com/cloudstore/cloudstore/issues/25">issue 25</a>.
147         * @return those {@link RepoFile}s which were modified after the given {@code localRevision}. Never
148         * <code>null</code>, but maybe empty.
149         */
150        public Collection<RepoFile> getRepoFilesChangedAfterExclLastSyncFromRepositoryId(final long localRevision, final UUID exclLastSyncFromRepositoryId) {
151                assertNotNull(exclLastSyncFromRepositoryId, "exclLastSyncFromRepositoryId");
152                final Query query = pm().newNamedQuery(getEntityClass(), "getRepoFilesChangedAfter_localRevision_exclLastSyncFromRepositoryId");
153                try {
154                        long startTimestamp = System.currentTimeMillis();
155                        @SuppressWarnings("unchecked")
156                        Collection<RepoFile> repoFiles = (Collection<RepoFile>) query.execute(localRevision, exclLastSyncFromRepositoryId.toString());
157                        logger.debug("getRepoFilesChangedAfter: query.execute(...) took {} ms.", System.currentTimeMillis() - startTimestamp);
158
159                        startTimestamp = System.currentTimeMillis();
160                        repoFiles = load(repoFiles);
161                        logger.debug("getRepoFilesChangedAfter: Loading result-set with {} elements took {} ms.", repoFiles.size(), System.currentTimeMillis() - startTimestamp);
162
163                        return repoFiles;
164                } finally {
165                        query.closeAll();
166                }
167        }
168
169        @Override
170        public void deletePersistent(final RepoFile entity) {
171                getPersistenceManager().flush();
172                if (entity instanceof Directory)
173                        getDirectoryCache().remove((Directory) entity);
174
175                super.deletePersistent(entity);
176                getPersistenceManager().flush(); // We run *sometimes* into foreign key violations if we don't delete immediately :-(
177        }
178
179        private DirectoryCache getDirectoryCache() {
180                if (directoryCache == null)
181                        directoryCache = new DirectoryCache();
182
183                return directoryCache;
184        }
185}