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}