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