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}