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