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.PersistenceManager;
012import javax.jdo.Query;
013
014import org.slf4j.Logger;
015import org.slf4j.LoggerFactory;
016
017import co.codewizards.cloudstore.core.oio.File;
018import co.codewizards.cloudstore.core.util.AssertUtil;
019
020public class RepoFileDao extends Dao<RepoFile, RepoFileDao> {
021        private static final Logger logger = LoggerFactory.getLogger(RepoFileDao.class);
022
023        private Directory localRootDirectory;
024
025        private DirectoryCache directoryCache;
026
027        private static class DirectoryCache {
028                private static final int MAX_SIZE = 50;
029                private final Map<File, Directory> file2DirectoryCache = new HashMap<File, Directory>();
030                private final Map<Directory, File> directory2FileCache = new HashMap<Directory, File>();
031                private final LinkedList<Directory> directoryCacheList = new LinkedList<Directory>();
032
033                public Directory get(final File file) {
034                        return file2DirectoryCache.get(file);
035                }
036
037                public void put(final File file, final Directory directory) {
038                        file2DirectoryCache.put(assertNotNull(file, "file"), assertNotNull(directory, "directory"));
039                        directory2FileCache.put(directory, file);
040                        directoryCacheList.remove(directory);
041                        directoryCacheList.addLast(directory);
042                        removeOldEntriesIfNecessary();
043                }
044
045                public void remove(final Directory directory) {
046                        final File file = directory2FileCache.remove(directory);
047                        file2DirectoryCache.remove(file);
048                }
049
050                public void remove(final File file) {
051                        final Directory directory = file2DirectoryCache.remove(file);
052                        directory2FileCache.remove(directory);
053                }
054
055                private void removeOldEntriesIfNecessary() {
056                        while (directoryCacheList.size() > MAX_SIZE) {
057                                final Directory directory = directoryCacheList.removeFirst();
058                                remove(directory);
059                        }
060                }
061        }
062
063        /**
064         * Get the child of the given {@code parent} with the specified {@code name}.
065         * @param parent the {@link RepoFile#getParent() parent} of the queried child.
066         * @param name the {@link RepoFile#getName() name} of the queried child.
067         * @return the child matching the given criteria; <code>null</code>, if there is no such object in the database.
068         */
069        public RepoFile getChildRepoFile(final RepoFile parent, final String name) {
070                final Query query = pm().newNamedQuery(getEntityClass(), "getChildRepoFile_parent_name");
071                final RepoFile repoFile = (RepoFile) query.execute(parent, name);
072                return repoFile;
073        }
074
075        /**
076         * Get the {@link RepoFile} for the given {@code file} in the file system.
077         * @param localRoot the repository's root directory in the file system. Must not be <code>null</code>.
078         * @param file the file in the file system for which to query the associated {@link RepoFile}. Must not be <code>null</code>.
079         * @return the {@link RepoFile} for the given {@code file} in the file system; <code>null</code>, if no such
080         * object exists in the database.
081         * @throws IllegalArgumentException if one of the parameters is <code>null</code> or if the given {@code file}
082         * is not located inside the repository - i.e. it is not a direct or indirect child of the given {@code localRoot}.
083         */
084        public RepoFile getRepoFile(final File localRoot, final File file) throws IllegalArgumentException {
085                return _getRepoFile(AssertUtil.assertNotNull(localRoot, "localRoot"), AssertUtil.assertNotNull(file, "file"), file);
086        }
087
088        private RepoFile _getRepoFile(final File localRoot, final File file, final File originallySearchedFile) {
089                if (localRoot.equals(file)) {
090                        return getLocalRootDirectory();
091                }
092
093                final DirectoryCache directoryCache = getDirectoryCache();
094                final Directory directory = directoryCache.get(file);
095                if (directory != null)
096                        return directory;
097
098                final File parentFile = file.getParentFile();
099                if (parentFile == null)
100                        throw new IllegalArgumentException(String.format("Repository '%s' does not contain file '%s'!", localRoot, originallySearchedFile));
101
102                final RepoFile parentRepoFile = _getRepoFile(localRoot, parentFile, originallySearchedFile);
103                final RepoFile result = getChildRepoFile(parentRepoFile, file.getName());
104                if (result instanceof Directory)
105                        directoryCache.put(file, (Directory)result);
106
107                return result;
108        }
109
110        public Directory getLocalRootDirectory() {
111                if (localRootDirectory == null)
112                        localRootDirectory = new LocalRepositoryDao().persistenceManager(pm()).getLocalRepositoryOrFail().getRoot();
113
114                return localRootDirectory;
115        }
116
117        /**
118         * Get the children of the given {@code parent}.
119         * <p>
120         * The children are those {@link RepoFile}s whose {@link RepoFile#getParent() parent} equals the given
121         * {@code parent} parameter.
122         * @param parent the parent whose children are to be queried. This may be <code>null</code>, but since
123         * there is only one single instance with {@code RepoFile.parent} being null - the root directory - this
124         * is usually never <code>null</code>.
125         * @return the children of the given {@code parent}. Never <code>null</code>, but maybe empty.
126         */
127        public Collection<RepoFile> getChildRepoFiles(final RepoFile parent) {
128                final Query query = pm().newNamedQuery(getEntityClass(), "getChildRepoFiles_parent");
129                try {
130                        @SuppressWarnings("unchecked")
131                        final 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 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("getRepoFilesChangedAfter: query.execute(...) took {} ms.", System.currentTimeMillis() - startTimestamp);
161
162                        fetchPlanBackup.restore(pm);
163                        startTimestamp = System.currentTimeMillis();
164                        repoFiles = load(repoFiles);
165                        logger.debug("getRepoFilesChangedAfter: 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}