001package co.codewizards.cloudstore.local.persistence; 002 003import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; 004import static co.codewizards.cloudstore.core.util.Util.*; 005 006import java.util.Collections; 007import java.util.Date; 008import java.util.LinkedList; 009import java.util.List; 010import java.util.UUID; 011 012import javax.jdo.JDOHelper; 013import javax.jdo.annotations.Discriminator; 014import javax.jdo.annotations.DiscriminatorStrategy; 015import javax.jdo.annotations.Index; 016import javax.jdo.annotations.Indices; 017import javax.jdo.annotations.NullValue; 018import javax.jdo.annotations.PersistenceCapable; 019import javax.jdo.annotations.Persistent; 020import javax.jdo.annotations.Queries; 021import javax.jdo.annotations.Query; 022import javax.jdo.annotations.Unique; 023 024import org.slf4j.Logger; 025import org.slf4j.LoggerFactory; 026 027import co.codewizards.cloudstore.core.oio.File; 028import co.codewizards.cloudstore.core.util.AssertUtil; 029 030@PersistenceCapable 031@Discriminator(strategy=DiscriminatorStrategy.VALUE_MAP) 032@Unique(name="RepoFile_parent_name", members={"parent", "name"}) 033@Indices({ 034 @Index(name="RepoFile_parent", members={"parent"}), 035 @Index(name="RepoFile_localRevision", members={"localRevision"}) 036}) 037@Queries({ 038 @Query(name="getChildRepoFile_parent_name", value="SELECT UNIQUE WHERE this.parent == :parent && this.name == :name"), 039 @Query(name="getChildRepoFiles_parent", value="SELECT WHERE this.parent == :parent"), 040 @Query( 041 name="getRepoFilesChangedAfter_localRevision_exclLastSyncFromRepositoryId", 042 value="SELECT WHERE this.localRevision > :localRevision && (this.lastSyncFromRepositoryId == null || this.lastSyncFromRepositoryId != :lastSyncFromRepositoryId)") // TODO this necessary == null is IMHO a DN bug! 043}) 044public abstract class RepoFile extends Entity implements AutoTrackLocalRevision { 045 private static final Logger logger = LoggerFactory.getLogger(RepoFile.class); 046 047 private RepoFile parent; 048 049 @Persistent(nullValue=NullValue.EXCEPTION) 050 private String name; 051 052 private long localRevision; 053 054 @Persistent(nullValue = NullValue.EXCEPTION) 055 private Date lastModified; 056 057 // TODO 1: The direct partner-repository from which this was synced, should be a real relation to the RemoteRepository, 058 // because this is more efficient (not a String, but a long id). 059 // TODO 2: We should additionally store (and forward) the origin repositoryId (UUID/String) to use this feature during 060 // circular syncs over multiple repos - e.g. repoA ---> repoB ---> repoC ---> repoA (again) - this circle would currently 061 // cause https://github.com/cloudstore/cloudstore/issues/25 again (because issue 25 is only solved for direct partners - not indirect). 062 // TODO 3: We should switch from UUID to Uid everywhere (most importantly the repositoryId). 063 // Careful, though: Uid's String-representation is case-sensitive! Due to Windows, it must thus not be used for file names! 064 private String lastSyncFromRepositoryId; 065 066 public RepoFile getParent() { 067 return parent; 068 } 069 public void setParent(final RepoFile parent) { 070 if (! equal(this.parent, parent)) 071 this.parent = parent; 072 } 073 074 public String getName() { 075 return name; 076 } 077 public void setName(final String name) { 078 if (! equal(this.name, name)) 079 this.name = name; 080 } 081 082 /** 083 * {@inheritDoc} 084 * <p> 085 * Note that this does not include modifications of children (in case this is a directory). 086 * If a child is modified, solely this child's localRevision is updated. 087 */ 088 @Override 089 public long getLocalRevision() { 090 return localRevision; 091 } 092 @Override 093 public void setLocalRevision(final long localRevision) { 094 if (! equal(this.localRevision, localRevision)) { 095 if (logger.isDebugEnabled()) { 096 final LocalRepository localRepository = new LocalRepositoryDao().persistenceManager(JDOHelper.getPersistenceManager(this)).getLocalRepositoryOrFail(); 097 logger.debug("setLocalRevision: localRepositoryId={} path='{}' old={} new={}", localRepository.getRepositoryId(), getPath(), this.localRevision, localRevision); 098 } 099 this.localRevision = localRevision; 100 } 101 } 102 103 /** 104 * Gets the path within the repository from the {@link LocalRepository#getRoot() root} (including) to <code>this</code> (including). 105 * <p> 106 * The first element in the list is the {@code root}. The last element is <code>this</code>. 107 * <p> 108 * If this method is called on the {@code root} itself, the result will be a list with one single element (the root itself). 109 * @return the path within the repository from the {@link LocalRepository#getRoot() root} (including) to <code>this</code> (including). Never <code>null</code>. 110 */ 111 public List<RepoFile> getPathList() { 112 final LinkedList<RepoFile> path = new LinkedList<RepoFile>(); 113 RepoFile rf = this; 114 while (rf != null) { 115 path.addFirst(rf); 116 rf = rf.getParent(); 117 } 118 return Collections.unmodifiableList(path); 119 } 120 121 /** 122 * Gets the path from the root to <code>this</code>. 123 * <p> 124 * The path's elements are separated by a slash ("/"). The path starts with a slash (like an absolute path), but 125 * is relative to the repository's local root. 126 * @return the path from the root to <code>this</code>. Never <code>null</code>. The repository's root itself has the path "/". 127 */ 128 public String getPath() { 129 final StringBuilder sb = new StringBuilder(); 130 for (final RepoFile repoFile : getPathList()) { 131 if (sb.length() == 0 || sb.charAt(sb.length() - 1) != '/') 132 sb.append('/'); 133 134 sb.append(repoFile.getName()); 135 } 136 return sb.toString(); 137 } 138 139 /** 140 * Gets the {@link File} represented by this {@link RepoFile} inside the given repository's {@code localRoot} directory. 141 * @param localRoot the repository's root directory. 142 * @return the {@link File} represented by this {@link RepoFile} inside the given repository's {@code localRoot} directory. 143 */ 144 public File getFile(final File localRoot) { 145 AssertUtil.assertNotNull(localRoot, "localRoot"); 146 File result = localRoot; 147 for (final RepoFile repoFile : getPathList()) { 148 if (repoFile.getParent() == null) // skip the root 149 continue; 150 151 result = createFile(result, repoFile.getName()); 152 } 153 return result; 154 } 155 156 /** 157 * Gets the timestamp of the file's last modification. 158 * <p> 159 * It reflects the {@link File#lastModified() File.lastModified} property. 160 * @return the timestamp of the file's last modification. 161 */ 162 public Date getLastModified() { 163 return lastModified; 164 } 165 public void setLastModified(final Date lastModified) { 166 if (! equal(this.lastModified, lastModified)) 167 this.lastModified = lastModified; 168 } 169 170 public UUID getLastSyncFromRepositoryId() { 171 return lastSyncFromRepositoryId == null ? null : UUID.fromString(lastSyncFromRepositoryId); 172 } 173 public void setLastSyncFromRepositoryId(final UUID repositoryId) { 174 if (! equal(this.getLastSyncFromRepositoryId(), repositoryId)) 175 this.lastSyncFromRepositoryId = repositoryId == null ? null : repositoryId.toString(); 176 } 177}